server-routes について
TanStack Routerでアプリのルートと並行してHTTPエンドポイントを作成するためのスキルです。ファイルベースの規約、リクエスト処理、ミドルウェア、安全面の注意点を扱い、明確なHTTP契約が必要な開発者向けです。
主な機能
- 01ファイルベースのサーバールート規約
- 02HTTPメソッドハンドラー
- 03リクエストとレスポンスの処理
- 04ルートおよびハンドラーのミドルウェア
- 05動的パラメーターとワイルドカード
活用シーン
- 01アプリのルートと並行してHTTP APIを公開
- 02JSONボディとステータスコードを処理
- 03認証や検証のミドルウェアを適用
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ tanstack/ router --skill server-routesインストール後、Claude Code で/server-routesを入力して読み込まれたか確認します。
詳細
サーバールート
サーバールートは、src/routes ディレクトリ内でアプリルートと並んで定義される API エンドポイントです。createFileRoute の server プロパティを使用し、生の HTTP リクエストを処理します。
呼び出し元に HTTP コントラクトが必要な場合は、サーバールートを使用してください。Start アプリケーション内だけで使用するデータには、サーバー関数を使用し、ローダーから直接呼び出してください。ルートローダーは SSR 中およびクライアントナビゲーション中に実行されるため、fetch('/api/...') はポータブルなローダーパターンではありません。
基本的なサーバールート
// src/routes/api/hello.ts
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/api/hello')({
server: {
handlers: {
GET: async ({ request }) => {
return new Response('Hello, World!')
},
},
},
})
サーバールートとアプリルートの組み合わせ
同じファイルにサーバールートと UI ルートの両方を定義できます。
// src/routes/hello.tsx
import { createFileRoute } from '@tanstack/react-router'
import { useState } from 'react'
export const Route = createFileRoute('/hello')({
server: {
handlers: {
POST: async ({ request }) => {
const body = await request.json()
return Response.json({ message: `Hello, ${body.name}!` })
},
},
},
component: HelloComponent,
})
function HelloComponent() {
const [reply, setReply] = useState('')
return (
<button
onClick={() => {
fetch('/hello', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Tanner' }),
})
.then((res) => res.json())
.then((data) => setReply(data.message))
}}
>
Say Hello {reply && `- ${reply}`}
</button>
)
}
上記の相対 fetch('/hello') は、ブラウザーのクリックハンドラー内でのみ実行されるため安全です。
Start ルートとのデータ共有
SSR ローダーから自身のサーバールートを呼び出さないでください。ビジネス処理をサーバー専用サービスに配置し、両方が必要な場合は両方の境界から公開してください。
// src/server/issues.server.ts
export function listIssues() {
return db.issues.findMany()
}
// src/routes/api/issues.ts
export const Route = createFileRoute('/api/issues')({
server: {
handlers: {
GET: async () => Response.json(await listIssues()),
},
},
})
// src/routes/issues.tsx
const getIssues = createServerFn({ method: 'GET' }).handler(() => {
return listIssues()
})
export const Route = createFileRoute('/issues')({
loader: () => getIssues(),
})
これにより、SSR が URL 解決に依存しなくなり、ビジネスロジックのソースを 1 つに保てます。
ファイルルートの規約
サーバールートは、TanStack Router のファイルベースルーティング規約に従います。
| ファイル | ルート |
|---|---|
routes/users.ts | /users |
routes/users/$id.ts | /users/$id |
routes/users/$id/posts.ts | /users/$id/posts |
routes/api/file/$.ts | /api/file/$(スプラット) |
routes/my-script[.]js.ts | /my-script.js(エスケープされたドット) |
一意のルートパス
各ルートに設定できるハンドラーファイルは 1 つだけです。次のファイルは競合します。
routes/users.tsroutes/users.index.tsroutes/users/index.ts
ハンドラーコンテキスト
各ハンドラーは、次の情報を受け取ります。
request— 受信した Request オブジェクトparams— 動的なパスパラメーターcontext— ミドルウェアからのコンテキストpathname— マッチしたパス名next— SSR に処理を渡すための呼び出し(Responseを返します)
動的パスパラメーター
// routes/users/$id.ts
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/users/$id')({
server: {
handlers: {
GET: async ({ params }) => {
return new Response(`User ID: ${params.id}`)
},
},
},
})
スプラット/ワイルドカードパラメーター
// routes/file/$.ts
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/file/$')({
server: {
handlers: {
GET: async ({ params }) => {
return new Response(`File: ${params._splat}`)
},
},
},
})
リクエストボディの処理
export const Route = createFileRoute('/api/users')({
server: {
handlers: {
POST: async ({ request }) => {
const body = await request.json()
return Response.json({ created: body.name })
},
},
},
})
その他のボディメソッド:request.text()、request.formData()。
JSON レスポンス
// Using Response.json helper
handlers: {
GET: async () => {
return Response.json({ message: 'Hello!' })
},
}
ステータスコードとヘッダー
handlers: {
GET: async ({ params }) => {
const user = await findUser(params.id)
if (!user) {
return new Response('Not found', { status: 404 })
}
return Response.json(user)
},
}
handlers: {
GET: async () => {
return new Response('Hello', {
headers: { 'Content-Type': 'text/plain' },
})
},
}
サーバールートでのミドルウェア
すべてのハンドラー
export const Route = createFileRoute('/api/admin')({
server: {
middleware: [authMiddleware, loggerMiddleware],
handlers: {
GET: async ({ context }) => Response.json(context.user),
POST: async ({ request, context }) => {
/* ... */
},
},
},
})
createHandlers を使用した特定のハンドラー
export const Route = createFileRoute('/api/data')({
server: {
handlers: ({ createHandlers }) =>
createHandlers({
GET: async () => Response.json({ public: true }),
POST: {
middleware: [authMiddleware],
handler: async ({ context }) => {
return Response.json({ user: context.session.user })
},
},
}),
},
})
ルートレベルとハンドラー固有の設定の組み合わせ
export const Route = createFileRoute('/api/posts')({
server: {
middleware: [authMiddleware], // runs first for all
handlers: ({ createHandlers }) =>
createHandlers({
GET: async () => Response.json([]),
POST: {
middleware: [validationMiddleware], // runs after auth, POST only
handler: async ({ request }) => {
const body = await request.json()
return Response.json({ created: true })
},
},
}),
},
})
よくある間違い
1. 重大:ページは保護しても、サーバールートを保護していない
プライベートデータを読み取る、または書き込むすべてのハンドラーで、ルートミドルウェア、ハンドラーミドルウェア、またはハンドラー内のチェックを通じて、リクエストを認証・認可する必要があります。ルーターの beforeLoad リダイレクトでは /api/... は保護されません。Cookie なしでハンドラーを直接テストし、プライベートなペイロードが返されないことを確認してください。
2. 中程度:ルートパスの重複
# WRONG — both resolve to /users, causes error
routes/users.ts
routes/users/index.ts
# CORRECT — pick one
routes/users.ts
3. 中程度:リクエストボディメソッドの await 忘れ
// WRONG — body is a Promise, not the actual data
const body = request.json()
// CORRECT — await the promise
const body = await request.json()
4. 高:TypeScript をレスポンススキーマの検証として信頼する
リクエスト入力を検証し、シリアライズされた Response の出力をテストしてください。型付けされたサービスでも、新しく追加されたフィールドが射影またはシリアライズされない場合があります。スキーマを変更した場合は、サーバールートの境界で await response.json() をアサートしてください。
関連項目
- start-core/middleware — サーバールート用のミドルウェア
- start-core/server-functions — RPC スタイルの呼び出しに代わる方法