关于 server-routes
面向 TanStack Router 开发者的技能,用于在应用路由旁构建 HTTP 端点。内容涵盖文件路由约定、请求处理、中间件和安全注意事项,适合需要明确 HTTP 契约的场景。
主要功能
- 01基于文件的服务端路由约定
- 02HTTP 方法处理器
- 03请求与响应处理
- 04路由及处理器中间件
- 05动态参数与通配符参数
适用场景
- 01在应用路由旁提供 HTTP API
- 02处理 JSON 请求体和状态码
- 03应用身份验证或校验中间件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tanstack/ router --skill server-routes安装后在 Claude Code 中输入/server-routes确认已加载。
详细介绍
服务端路由
服务端路由是在 src/routes 目录中与应用路由一同定义的 API 端点。它们使用 createFileRoute 上的 server 属性,并处理原始 HTTP 请求。
当调用方需要 HTTP 契约时,请使用服务端路由。对于仅供 Start 应用使用的数据,优先使用服务端函数,并在 loader 中直接调用。路由 loader 会在 SSR 和客户端导航期间运行,因此 fetch('/api/...') 并不是可移植的 loader 模式。
基本服务端路由
// 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 loader 调用自己的服务端路由。将业务操作放入仅服务端使用的服务中,然后在两种边界都需要时,通过它们分别对外提供:
// 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 解析,并确保业务逻辑只有一个来源。
文件路由约定
服务端路由遵循 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(转义点号) |
唯一路由路径
每个路由只能有一个处理程序文件。以下文件会发生冲突:
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. 中等:忘记等待请求正文方法完成
// 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 风格调用的替代方案