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 方法——GETPOSTPUTPATCHDELETEHEADOPTIONS

// app/api/chat/route.ts
export async function GET(request: Request) { /* ... */ }
export async function POST(request: Request) { /* ... */ }
route.ts 和 page.tsx 不能共存

同一目录下不能同时有 route.tspage.tsx——它们会争夺同一个路由。API 路由通常放在 app/api/ 下和页面路由分开。

1.2 什么时候用 Route Handlers

Route Handlers 和 Server Actions 都在服务端执行,但适用场景不同:

场景推荐原因
表单提交、数据变更Server Actions渐进增强、自动 revalidate
第三方 Webhook 接收Route Handlers外部系统只能发 HTTP 请求
给移动端/第三方提供 APIRoute 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 中 paramsPromise

// 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))
}
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 进阶实战。