middleware について
TanStack Start 向けに、リクエストとサーバー関数のミドルウェアを構成し、検証やコンテキスト、認可を扱う方法を解説します。
主な機能
- 01リクエストと関数のミドルウェア
- 02合成可能なミドルウェアチェーン
- 03クライアントとサーバーのフェーズ
- 04検証とコンテキストの受け渡し
- 05ルートとグローバルのミドルウェア
活用シーン
- 01サーバー関数にセッション確認を追加
- 02ミドルウェア間でコンテキストを共有
- 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 エンドポイントのハンドラー/ミドルウェアで行います