mcpField
M
middleware

middleware

github.com/tanstack開発15.3k195 回インストール4 日前に更新
リポジトリ

middleware について

TanStack Start 向けに、リクエストとサーバー関数のミドルウェアを構成し、検証やコンテキスト、認可を扱う方法を解説します。

主な機能

  1. 01リクエストと関数のミドルウェア
  2. 02合成可能なミドルウェアチェーン
  3. 03クライアントとサーバーのフェーズ
  4. 04検証とコンテキストの受け渡し
  5. 05ルートとグローバルのミドルウェア

活用シーン

  1. 01サーバー関数にセッション確認を追加
  2. 02ミドルウェア間でコンテキストを共有
  3. 03ルートハンドラーにミドルウェアを適用

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/tanstack/router --skill middleware

インストール後、Claude Code で/middlewareを入力して読み込まれたか確認します。

詳細

ミドルウェア

ミドルウェアはサーバー関数とサーバールートの動作をカスタマイズします。合成可能であり、ミドルウェアがほかのミドルウェアに依存してチェーンを形成できます。

CRITICAL: TypeScript ではメソッドの順序が強制されます:middleware() → validator() → client() → server()。順序が正しくないと型エラーが発生します。 CRITICAL: sendContext の「形状」を検証しても(例:z.string().uuid().parse(...))、認可にはなりません。解析済みの識別子は形式が正しい識別子であり、認可された識別子ではありません。クライアントから送信された ID をクエリーキー、フィルター、パスパラメーターとして使用する前に、必ずセッションのプリンシパルに照らしてアクセス権を再確認してください。

2 種類のミドルウェア

機能リクエストミドルウェアサーバー関数ミドルウェア
適用範囲すべてのサーバーリクエスト(SSR、ルート、関数)サーバー関数のみ
メソッド.server().client()、.server()
入力検証いいえはい(.validator())
クライアント側のロジックいいえはい
作成方法createMiddleware()createMiddleware({ type: 'function' })

リクエストミドルウェアはサーバー関数ミドルウェアに依存できません。サーバー関数ミドルウェアは両方の種類に依存できます。

リクエストミドルウェア

すべてのサーバーリクエスト(SSR、サーバールート、サーバー関数)で実行されます:

// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createMiddleware } from '@tanstack/react-start'

const loggingMiddleware = createMiddleware().server(
  async ({ next, context, request }) => {
    console.log('Request:', request.url)
    const result = await next()
    return result
  },
)

サーバー関数ミドルウェア

クライアントフェーズとサーバーフェーズの両方があります:

// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createMiddleware } from '@tanstack/react-start'

const authMiddleware = createMiddleware({ type: 'function' })
  .client(async ({ next }) => {
    // Runs on client BEFORE the RPC call
    const result = await next()
    // Runs on client AFTER the RPC response
    return result
  })
  .server(async ({ next, context }) => {
    // Runs on server BEFORE the handler
    const result = await next()
    // Runs on server AFTER the handler
    return result
  })

サーバー関数へのミドルウェアの適用

// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createServerFn } from '@tanstack/react-start'

const fn = createServerFn()
  .middleware([authMiddleware])
  .handler(async ({ context }) => {
    // context contains data from middleware
    return { user: context.user }
  })

next() によるコンテキストの受け渡し

ミドルウェアチェーン内でコンテキストを受け渡します:

const authMiddleware = createMiddleware().server(async ({ next, request }) => {
  const session = await getSession(request.headers)
  if (!session) throw new Error('Unauthorized')

  return next({
    context: { session },
  })
})

const roleMiddleware = createMiddleware()
  .middleware([authMiddleware])
  .server(async ({ next, context }) => {
    console.log('Session:', context.session) // typed!
    return next()
  })

クライアントとサーバー間のコンテキスト送信

クライアント → サーバー(sendContext)
const workspaceMiddleware = createMiddleware({ type: 'function' })
  .client(async ({ next, context }) => {
    return next({
      sendContext: {
        workspaceId: context.workspaceId,
      },
    })
  })
  .server(async ({ next, context }) => {
    // workspaceId available here, but VALIDATE IT
    console.log('Workspace:', context.workspaceId)
    return next()
  })
サーバー → クライアント(サーバー内の sendContext)
const serverTimer = createMiddleware({ type: 'function' }).server(
  async ({ next }) => {
    return next({
      sendContext: {
        timeFromServer: new Date(),
      },
    })
  },
)

const clientLogger = createMiddleware({ type: 'function' })
  .middleware([serverTimer])
  .client(async ({ next }) => {
    const result = await next()
    console.log('Server time:', result.context.timeFromServer)
    return result
  })

ミドルウェアでの入力検証

import { z } from 'zod'
import { zodValidator } from '@tanstack/zod-adapter'

const workspaceMiddleware = createMiddleware({ type: 'function' })
  .validator(zodValidator(z.object({ workspaceId: z.string() })))
  .server(async ({ next, data }) => {
    console.log('Workspace:', data.workspaceId)
    return next()
  })

グローバルミドルウェア

グローバルミドルウェアを設定するには、src/start.ts を作成します:

// src/start.ts
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import { createStart, createMiddleware } from '@tanstack/react-start'

const requestLogger = createMiddleware().server(async ({ next, request }) => {
  console.log(`${request.method} ${request.url}`)
  return next()
})

const functionAuth = createMiddleware({ type: 'function' }).server(
  async ({ next }) => {
    // runs for every server function
    return next()
  },
)

export const startInstance = createStart(() => ({
  requestMiddleware: [requestLogger],
  functionMiddleware: [functionAuth],
}))

サーバールートでのミドルウェアの使用

ルート内のすべてのハンドラー
export const Route = createFileRoute('/api/users')({
  server: {
    middleware: [authMiddleware],
    handlers: {
      GET: async ({ context }) => Response.json(context.user),
      POST: async ({ request }) => {
        /* ... */
      },
    },
  },
})
特定のハンドラーのみ
export const Route = createFileRoute('/api/users')({
  server: {
    handlers: ({ createHandlers }) =>
      createHandlers({
        GET: async () => Response.json({ public: true }),
        POST: {
          middleware: [authMiddleware],
          handler: async ({ context }) => {
            return Response.json({ user: context.session.user })
          },
        },
      }),
  },
})

ミドルウェアファクトリー

認可などのパターンを再利用できるよう、パラメーター化されたミドルウェアを作成します:

const authMiddleware = createMiddleware().server(async ({ next, request }) => {
  const session = await auth.getSession({ headers: request.headers })
  if (!session) throw new Error('Unauthorized')
  return next({ context: { session } })
})

認証が必要なすべての createServerFn に authMiddleware を適用してください。 サーバー関数は API エンドポイントです。ルートの beforeLoad で保護されるのはルートの UI のみであり、そのデータは保護されません。プライベートデータを読み取る、または変更するエンドポイントを保護してください。@tanstack/router-core#router-core/auth-and-guards と start-core/auth-server-primitives を参照してください。

type Permissions = Record<string, string[]>

function authorizationMiddleware(permissions: Permissions) {
  return createMiddleware({ type: 'function' })
    .middleware([authMiddleware])
    .server(async ({ next, context }) => {
      const granted = await auth.hasPermission(context.session, permissions)
      if (!granted) throw new Error('Forbidden')
      return next()
    })
}

// Usage
const getClients = createServerFn()
  .middleware([authorizationMiddleware({ client: ['read'] })])
  .handler(async () => {
    return { message: 'The user can read clients.' }
  })

カスタムヘッダーと fetch

クライアントミドルウェアからのヘッダー設定
const authMiddleware = createMiddleware({ type: 'function' }).client(
  async ({ next }) => {
    return next({
      headers: { Authorization: `Bearer ${getToken()}` },
    })
  },
)

ヘッダーはミドルウェア間でマージされます。後続のミドルウェアが先行するミドルウェアを上書きします。呼び出し元で指定したヘッダーは、すべてのミドルウェアのヘッダーを上書きします。

カスタム fetch
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
import type { CustomFetch } from '@tanstack/react-start'

const loggingMiddleware = createMiddleware({ type: 'function' }).client(
  async ({ next }) => {
    const customFetch: CustomFetch = async (url, init) => {
      console.log('Request:', url)
      return fetch(url, init)
    }
    return next({ fetch: customFetch })
  },
)

fetch の優先順位(高い順):呼び出し元 → 後続のミドルウェア → 先行するミドルウェア → createStart のグローバル設定 → デフォルトの fetch。

よくある間違い

1. 重大:クライアントの sendContext を信頼する — 形状の確認はアクセス権の確認ではありません

クライアントミドルウェアからの sendContext は、信頼できないクライアント入力としてサーバーに届きます。多くのエージェントは Zod で形状を解析しただけで、値は安全だと判断してしまいます。そうではありません。解析済みの UUID は「何らかの」ワークスペースを指すものであり、リクエストしたユーザーのワークスペースとは限りません。セッションのプリンシパルに照らしたメンバーシップ確認を行わないと、テナントを横断できるエンドポイントを作ることになります。

レイヤー 1 — 誤り(検証なし):

.server(async ({ next, context }) => {
  // SQL-injectable AND tenant-walkable
  await db.query(`SELECT * FROM workspace_${context.workspaceId}`)
  return next()
})

レイヤー 2 — まだ誤り(形状のみ):

.server(async ({ next, context }) => {
  // Looks safe, isn't. UUID is well-formed but the user may not be a member.
  const workspaceId = z.string().uuid().parse(context.workspaceId)
  await db.query('SELECT * FROM workspaces WHERE id = $1', [workspaceId])
  return next()
})

レイヤー 3 — 正しい(形状とアクセス権の両方):

.middleware([authMiddleware]) // session loaded from cookie, NOT from sendContext
.server(async ({ next, context }) => {
  const workspaceId = z.string().uuid().parse(context.workspaceId)
  // Verify the session principal can access this workspace.
  const member = await db.memberships.find({
    userId: context.session.userId,
    workspaceId,
  })
  if (!member) throw new Error('Not a member of this workspace')
  await db.query('SELECT * FROM workspaces WHERE id = $1', [workspaceId])
  return next({ context: { workspaceId } })
})

セッション自体は、サーバーが信頼できる情報源(authMiddleware での Cookie と DB の照合)から取得する必要があります。sendContext から取得してはいけません。クライアントが送信できるものについては、クライアントが偽ることもできます。start-core/auth-server-primitives を参照してください。

2. MEDIUM:リクエストミドルウェアとサーバー関数ミドルウェアの混同

リクエストミドルウェアはすべてのリクエスト(SSR、ルート、関数)で実行されます。サーバー関数ミドルウェアは createServerFn の呼び出し時のみ実行され、.client() メソッドを備えています。

3. HIGH:.client() 内のブラウザー API による SSR 時のクラッシュ

SSR 中、.client() コールバックはサーバー上で実行されます。localStorage や window など、ブラウザーでのみ使える API は ReferenceError をスローします。

// WRONG — localStorage doesn't exist on the server during SSR
const middleware = createMiddleware({ type: 'function' }).client(
  async ({ next }) => {
    const token = localStorage.getItem('token')
    return next({ sendContext: { token } })
  },
)

// CORRECT — use cookies/headers or guard with typeof window check
const middleware = createMiddleware({ type: 'function' }).client(
  async ({ next }) => {
    const token =
      typeof window !== 'undefined' ? localStorage.getItem('token') : null
    return next({ sendContext: { token } })
  },
)
4. MEDIUM:メソッドの順序が誤っている
// WRONG — type error
createMiddleware({ type: 'function' })
  .server(() => { ... })
  .client(() => { ... })

// CORRECT — middleware → validator → client → server
createMiddleware({ type: 'function' })
  .middleware([dep])
  .validator(schema)
  .client(({ next }) => next())
  .server(({ next }) => next())

関連項目

  • start-core/server-functions — ミドルウェアが何をラップするか
  • start-core/server-routes — API エンドポイントでのミドルウェア
  • start-core/auth-server-primitives — authMiddleware ファクトリー自体の構築:セッション Cookie の読み取り、OAuth の状態、CSRF
  • @tanstack/router-core#router-core/auth-and-guards — ルーティング側の UX ガード。データ認証はサーバー関数、サーバールート、または API エンドポイントのハンドラー/ミドルウェアで行います

コメント