Route Handlers 深度实战
Route Handlers 是 App Router 中构建 API 的方式——替代 Pages Router 的
pages/api。本章讲清请求解析、响应构建、流式输出、错误处理的完整实践,以及 Route Handlers 和 Server Actions 的选择策略。
1. Route Handlers 基础
1.1 文件约定
Route Handlers 定义在 app 目录下的 route.ts(或 route.js)文件中。文件路径即 API 路径:
app/
├── api/
│ ├── chat/
│ │ └── route.ts → GET/POST /api/chat
│ ├── chat/[id]/
│ │ └── route.ts → GET/PUT/DELETE /api/chat/:id
│ └── webhook/
│ └── route.ts → POST /api/webhook
导出函数名就是 HTTP 方法——GET、POST、PUT、PATCH、DELETE、HEAD、OPTIONS:
// app/api/chat/route.ts
export async function GET(request: Request) { /* ... */ }
export async function POST(request: Request) { /* ... */ }同一目录下不能同时有 route.ts 和 page.tsx——它们会争夺同一个路由。API 路由通常放在 app/api/ 下和页面路由分开。
1.2 什么时候用 Route Handlers
Route Handlers 和 Server Actions 都在服务端执行,但适用场景不同:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 表单提交、数据变更 | Server Actions | 渐进增强、自动 revalidate |
| 第三方 Webhook 接收 | Route Handlers | 外部系统只能发 HTTP 请求 |
| 给移动端/第三方提供 API | Route Handlers | 标准 REST 接口 |
| 文件下载/流式输出 | Route Handlers | 需要自定义 Response |
| AI 流式对话 | Route Handlers | 需要 ReadableStream |
| OAuth 回调 | Route Handlers | 需要处理 redirect |
简单说:内部变更用 Server Actions,对外接口和流式场景用 Route Handlers。
2. 请求解析
2.1 URL 参数
// app/api/chat/route.ts — 从 URL 读取查询参数
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const model = searchParams.get('model') // ?model=gpt-4o
const page = Number(searchParams.get('page')) || 1
const limit = Math.min(Number(searchParams.get('limit')) || 20, 100) // 限制最大值
const chats = await db.query.chats.findMany({
where: model ? eq(chats.model, model) : undefined,
limit,
offset: (page - 1) * limit,
orderBy: desc(chats.updatedAt),
})
return Response.json({ data: chats, page, limit })
}2.2 动态路由参数
动态路由参数通过第二个参数的 params 获取。注意在 Next.js 15 中 params 是 Promise:
// app/api/chat/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params
const chat = await db.query.chats.findFirst({
where: eq(chats.id, id),
})
if (!chat) {
return Response.json({ error: 'Chat not found' }, { status: 404 })
}
return Response.json(chat)
}2.3 请求体解析
根据 Content-Type 选择不同的解析方式:
export async function POST(request: Request) {
// JSON 请求体
const body = await request.json()
const { messages, model } = body as { messages: Message[]; model: string }
// FormData(文件上传)
const formData = await request.formData()
const file = formData.get('file') as File
const name = formData.get('name') as string
// 纯文本
const text = await request.text()
// 二进制
const buffer = await request.arrayBuffer()
}2.4 Headers 与 Cookies
import { cookies, headers } from 'next/headers'
export async function GET(request: Request) {
// 方式一:从 request 对象读取
const authHeader = request.headers.get('authorization')
const userAgent = request.headers.get('user-agent')
// 方式二:用 Next.js 的 headers() / cookies()
const headerStore = await headers()
const cookieStore = await cookies()
const token = headerStore.get('x-api-key')
const sessionId = cookieStore.get('session-id')?.value
// ...
}3. 响应构建
3.1 JSON 响应
// 最常用:Response.json()
return Response.json({ data: chats, total: 100 })
// 带状态码和自定义 Header
return Response.json(
{ error: 'Unauthorized' },
{
status: 401,
headers: { 'WWW-Authenticate': 'Bearer' },
},
)3.2 重定向
import { redirect } from 'next/navigation'
export async function GET(request: Request) {
const session = await auth()
if (!session) redirect('/login')
// 或用 Response
return Response.redirect(new URL('/dashboard', request.url))
}3.3 设置 Cookie
import { cookies } from 'next/headers'
export async function POST(request: Request) {
const { workspaceId } = await request.json()
const cookieStore = await cookies()
cookieStore.set('workspace-id', workspaceId, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 30, // 30 天
path: '/',
})
return Response.json({ success: true })
}3.4 CORS 处理
外部调用你的 API 需要 CORS 头。可以在单个 Route 处理,也可以在 middleware.ts 统一处理:
// app/api/public/route.ts — 单个路由的 CORS
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
export async function OPTIONS() {
return new Response(null, { headers: corsHeaders })
}
export async function GET(request: Request) {
const data = await getPublicData()
return Response.json(data, { headers: corsHeaders })
}4. 流式响应
流式响应是 Route Handlers 最强大的能力——AI 对话、大文件生成、实时日志都依赖它。
4.1 基础流式
用 ReadableStream + TextEncoder 构建流式响应:
// app/api/stream/route.ts
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
controller.enqueue(encoder.encode(`data: chunk ${i}\n\n`))
await new Promise((resolve) => setTimeout(resolve, 500))
}
controller.close()
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
})
}4.2 AI 对话流式输出
这是 AI SaaS 最核心的 API——接收消息列表,返回 AI 流式回复:
// app/api/chat/route.ts
import { OpenAI } from 'openai'
const openai = new OpenAI()
export async function POST(request: Request) {
const { messages, model = 'gpt-4o' } = await request.json()
// 1. 调用 OpenAI(流式模式)
const completion = await openai.chat.completions.create({
model,
messages,
stream: true,
})
// 2. 转换为 ReadableStream
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
try {
for await (const chunk of completion) {
const content = chunk.choices[0]?.delta?.content
if (content) {
controller.enqueue(encoder.encode(content))
}
}
} catch (error) {
controller.error(error)
} finally {
controller.close()
}
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'no-cache',
'X-Content-Type-Options': 'nosniff',
},
})
}客户端消费这个流(对应第 25 章 ChatStore 的 sendMessage):
const res = await fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({ messages, model }),
signal: abortController.signal,
})
const reader = res.body!.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value)
appendToMessage(chunk)
}4.3 使用 AI SDK
Vercel 的 ai SDK 简化了流式处理的样板代码:
// app/api/chat/route.ts — 用 AI SDK 简化
import { openai } from '@ai-sdk/openai'
import { streamText } from 'ai'
export async function POST(request: Request) {
const { messages, model = 'gpt-4o' } = await request.json()
const result = streamText({
model: openai(model),
messages,
})
return result.toDataStreamResponse()
}AI SDK 自动处理了流的创建、编码、错误处理和 Response 构建——几行代码替代几十行。
5. 错误处理
5.1 统一错误格式
定义一个标准的错误响应格式,所有 API 保持一致:
// lib/api-error.ts
export class ApiError extends Error {
constructor(
public statusCode: number,
message: string,
public code?: string,
) {
super(message)
}
}
export function errorResponse(error: unknown) {
if (error instanceof ApiError) {
return Response.json(
{ error: error.message, code: error.code },
{ status: error.statusCode },
)
}
console.error('Unexpected error:', error)
return Response.json(
{ error: 'Internal Server Error' },
{ status: 500 },
)
}5.2 在 Route Handler 中使用
import { ApiError, errorResponse } from '@/lib/api-error'
export async function POST(request: Request) {
try {
const session = await auth()
if (!session) throw new ApiError(401, '请先登录', 'UNAUTHORIZED')
const body = await request.json()
if (!body.messages?.length) throw new ApiError(400, '消息不能为空', 'EMPTY_MESSAGES')
const workspace = await getCurrentWorkspace()
const quota = await getTokenQuota(workspace!.id)
if (quota.remaining <= 0) throw new ApiError(403, 'Token 配额已用完', 'QUOTA_EXCEEDED')
// ... 正常逻辑
return Response.json({ success: true })
} catch (error) {
return errorResponse(error)
}
}5.3 请求校验
用 zod 校验请求体,拒绝不合法输入:
import { z } from 'zod'
const chatRequestSchema = z.object({
messages: z.array(z.object({
role: z.enum(['user', 'assistant', 'system']),
content: z.string().min(1).max(10000),
})).min(1).max(100),
model: z.enum(['gpt-4o', 'gpt-4o-mini', 'claude-3.5-sonnet']).default('gpt-4o'),
temperature: z.number().min(0).max(2).default(0.7),
})
export async function POST(request: Request) {
try {
const body = await request.json()
const { messages, model, temperature } = chatRequestSchema.parse(body)
// 校验通过,安全使用 messages, model, temperature
// ...
} catch (error) {
if (error instanceof z.ZodError) {
return Response.json(
{ error: '请求参数无效', details: error.errors },
{ status: 400 },
)
}
return errorResponse(error)
}
}6. 缓存与性能
6.1 静态与动态
默认情况下,没有读取动态数据(cookies()、headers()、searchParams)的 GET 请求会被静态缓存。一旦使用动态函数就变为动态请求:
// ✅ 静态缓存 — build 时生成,CDN 分发
export async function GET() {
const models = await getAvailableModels()
return Response.json(models)
}
// ❌ 动态请求 — 每次都执行
export async function GET(request: Request) {
const session = await auth() // 读取了 cookies,变成动态
// ...
}6.2 手动控制缓存
// 强制动态
export const dynamic = 'force-dynamic'
// 设置 ISR 重验证间隔(秒)
export const revalidate = 3600
// 通过 Response Header 控制
return Response.json(data, {
headers: { 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400' },
})6.3 Rate Limiting
生产环境的 API 必须做限流,防止滥用:
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '60 s'), // 每分钟 10 次
})
export async function POST(request: Request) {
const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1'
const { success, remaining } = await ratelimit.limit(ip)
if (!success) {
return Response.json(
{ error: '请求过于频繁,请稍后再试' },
{ status: 429, headers: { 'X-RateLimit-Remaining': String(remaining) } },
)
}
// ... 正常逻辑
}本章小结
- 文件约定:
app/api/.../route.ts,导出函数名即 HTTP 方法 - 请求解析:URL 参数、动态路由、JSON/FormData、Headers/Cookies
- 流式响应:
ReadableStream构建流,AI 场景用 AI SDK 简化 - 错误处理:
ApiError统一格式 +zod校验请求体 - 缓存:无动态函数的 GET 默认静态缓存,
revalidate控制 ISR - 选择策略:内部变更用 Server Actions,对外接口和流式场景用 Route Handlers
下一章讲 Server Actions 进阶实战。