Next.js: API Routes 与 Route Handlers
最后更新:2026-08-26
Route Handlers 是 Next.js 的 API 层——当 Server Actions 不够用时,标准 RESTful 端点仍然是现代 web 的基石。
1. 你将学到
route.ts文件定义 GET/POST/PUT/DELETE 端点- Zod 校验请求体与查询参数
NextResponse格式化响应(json/redirect/rewrite)middleware.ts配置 matcher 与请求改写- CORS 跨域配置与 Rate Limiting 实现
2. 一个 DevOps 工程师的真实故事
(1) 痛点:团队在 5 个页面中重复实现了 8 次认证逻辑
Diana 在审核 TaskFlow 代码时发现:每个 API Route 的开头都是同样的 15 行认证代码——从 cookie 解析 session、验证 token、返回 401。8 个 API 端点 × 15 行 = 120 行重复代码。更糟的是,有 3 个端点忘了加认证检查,直接把用户数据暴露给了未登录请求。
| 问题 | 数据 |
|---|---|
| 重复认证代码 | 120 行(8 端点 × 15 行) |
| 未保护的端点 | 3 个 |
| 安全审计失败 | 2 次 |
| 修复时间 | 每次 4 小时 |
(2) Middleware 的解法
用
middleware.ts统一处理认证、日志、CORS——一个文件控制所有请求的生命周期。
// middleware.ts — 统一认证 + 日志
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('session-token')?.value
// 检查 API 请求的认证
if (request.nextUrl.pathname.startsWith('/api/')) {
if (!token) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
}
// 添加安全头
const response = NextResponse.next()
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
return response
}
export const config = {
matcher: '/api/:path*',
}
(3) 收益
| 维度 | 前(无 middleware) | 后(middleware) |
|---|---|---|
| 认证代码 | 120 行(分散) | 15 行(集中) |
| 未保护端点 | 3 个 | 0 个 |
| 安全审计 | ❌ 失败 | ✅ 通过 |
| 新增端点工作量 | 15 行认证 + 逻辑 | 仅逻辑 |
3. Route Handlers 基础
Route Handlers 通过 app/api/**/route.ts 文件定义,每个 HTTP 方法对应一个导出的异步函数:
| HTTP 方法 | 导出函数 | 路由文件 |
|---|---|---|
| GET | export async function GET() |
app/api/items/route.ts |
| POST | export async function POST() |
app/api/items/route.ts |
| PUT | export async function PUT() |
app/api/items/[id]/route.ts |
| DELETE | export async function DELETE() |
app/api/items/[id]/route.ts |
| PATCH | export async function PATCH() |
app/api/items/[id]/route.ts |
(1) 基本 GET 端点
// app/api/items/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
const items = await db.item.findMany()
return NextResponse.json(items)
}
(2) 动态路由参数
// app/api/items/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const item = await db.item.findUnique({ where: { id: Number(params.id) } })
if (!item) {
return NextResponse.json({ error: 'Item not found' }, { status: 404 })
}
return NextResponse.json(item)
}
(3) POST 创建资源
// app/api/items/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
const body = await request.json()
const item = await db.item.create({ data: body })
return NextResponse.json(item, { status: 201 })
}
▶ 示例:CRUD 完整 Route Handler(难度⭐⭐)
// app/api/todos/route.ts — GET + POST
import { NextRequest, NextResponse } from 'next/server'
const API = 'https://jsonplaceholder.typicode.com'
export async function GET() {
const todos = await fetch(`${API}/todos?_limit=5`).then(r => r.json())
return NextResponse.json(todos)
}
export async function POST(request: NextRequest) {
const body = await request.json()
const todo = await fetch(`${API}/todos`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(r => r.json())
return NextResponse.json(todo, { status: 201 })
}
// app/api/todos/[id]/route.ts — GET + PUT + DELETE
import { NextRequest, NextResponse } from 'next/server'
const API = 'https://jsonplaceholder.typicode.com'
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const todo = await fetch(`${API}/todos/${params.id}`).then(r => r.json())
if (!todo || todo.id === undefined) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
return NextResponse.json(todo)
}
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const body = await request.json()
const updated = await fetch(`${API}/todos/${params.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(r => r.json())
return NextResponse.json(updated)
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
await fetch(`${API}/todos/${params.id}`, { method: 'DELETE' })
return NextResponse.json({ success: true })
}
4. Zod 请求校验
REST API 的请求数据校验同样推荐使用 Zod——比手动 if 检查更简洁、类型安全。
(1) 请求体校验
// app/api/items/route.ts — Zod 校验 POST 请求体
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
const createItemSchema = z.object({
name: z.string().min(2).max(100),
price: z.number().positive('Price must be positive'),
category: z.enum(['electronics', 'clothing', 'food']),
tags: z.array(z.string()).max(5).optional(),
})
export async function POST(request: NextRequest) {
const body = await request.json()
const validated = createItemSchema.safeParse(body)
if (!validated.success) {
return NextResponse.json(
{ error: 'Validation failed', details: validated.error.flatten().fieldErrors },
{ status: 400 }
)
}
const item = await db.item.create({ data: validated.data })
return NextResponse.json(item, { status: 201 })
}
(2) 查询参数校验
// app/api/items/route.ts — Zod 校验查询参数
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
const querySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(10),
search: z.string().optional(),
})
export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl
const query = querySchema.safeParse({
page: searchParams.get('page'),
limit: searchParams.get('limit'),
search: searchParams.get('search'),
})
if (!query.success) {
return NextResponse.json(
{ error: 'Invalid query parameters', details: query.error.flatten().fieldErrors },
{ status: 400 }
)
}
const { page, limit, search } = query.data
const items = await db.item.findMany({
skip: (page - 1) * limit,
take: limit,
where: search ? { name: { contains: search } } : undefined,
})
return NextResponse.json({ items, page, limit })
}
▶ 示例:Zod 校验完整 API(难度⭐⭐)
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { revalidateTag } from 'next/cache'
const userSchema = z.object({
name: z.string().min(2, 'Name too short'),
email: z.string().email('Invalid email'),
role: z.enum(['admin', 'user', 'viewer']).default('user'),
})
export async function POST(request: NextRequest) {
const body = await request.json()
const validated = userSchema.safeParse(body)
if (!validated.success) {
return NextResponse.json(
{ error: 'Validation failed', fields: validated.error.flatten().fieldErrors },
{ status: 400 }
)
}
const user = await fetch('https://jsonplaceholder.typicode.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(validated.data),
}).then(r => r.json())
revalidateTag('users')
return NextResponse.json(user, { status: 201 })
}
5. NextResponse 响应格式
NextResponse 是 Next.js 提供的专用响应工具,支持多种响应格式:
| 方法 | 用途 | 示例 |
|---|---|---|
NextResponse.json(data, opts?) |
JSON 响应 | NextResponse.json({ id: 1 }, { status: 201 }) |
NextResponse.redirect(url) |
重定向 | NextResponse.redirect(new URL('/login', request.url)) |
NextResponse.next() |
继续中间件链 | return NextResponse.next() |
NextResponse.rewrite(url) |
服务端 URL 重写 | NextResponse.rewrite(new URL('/fallback', request.url)) |
(1) JSON 响应
// app/api/auth/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({ status: 'healthy', uptime: process.uptime() })
}
(2) 重定向
// app/api/redirect/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const target = request.nextUrl.searchParams.get('to') ?? '/'
return NextResponse.redirect(new URL(target, request.url))
}
(3) Rewrite(URL 重写)
// middleware.ts — 将 /products/old-slug 重写到新路径
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/products/old-')) {
const newPath = request.nextUrl.pathname.replace('/old-', '/')
return NextResponse.rewrite(new URL(newPath, request.url))
}
return NextResponse.next()
}
▶ 示例:响应格式对比(难度⭐)
// app/api/response-demo/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const format = request.nextUrl.searchParams.get('format') ?? 'json'
switch (format) {
case 'json':
return NextResponse.json({ message: 'Hello', timestamp: Date.now() })
case 'redirect':
return NextResponse.redirect(new URL('/api/response-demo?format=json', request.url))
case 'rewrite':
return NextResponse.rewrite(new URL('/api/response-demo?format=json', request.url))
default:
return NextResponse.json({ error: 'Unknown format' }, { status: 400 })
}
}
6. Middleware 中间件
middleware.ts 是 Next.js 的请求拦截器——在每个请求到达页面或 API 之前执行,支持路径匹配、请求改写、Header 注入、认证检查。
sequenceDiagram
participant Client as Browser
participant MW as middleware.ts
participant Route as Route Handler/Page
Client->>MW: 请求 /api/todos
MW->>MW: 匹配 matcher 规则
MW->>MW: 认证检查 / Header 注入
alt 通过
MW->>Route: NextResponse.next()
Route-->>Client: 正常响应
else 未认证
MW-->>Client: NextResponse.json(401)
else 重定向
MW-->>Client: NextResponse.redirect(/login)
end
| 配置 | 类型 | 说明 |
|---|---|---|
matcher |
string[] | 匹配路径模式(支持 glob) |
request.nextUrl |
URL | 当前请求的 URL 对象 |
request.cookies |
Map | 请求 cookies |
request.headers |
Headers | 请求头 |
NextResponse.next() |
Response | 继续正常处理 |
NextResponse.redirect() |
Response | 重定向到其他 URL |
(1) matcher 配置
// middleware.ts — matcher 配置
export const config = {
matcher: [
'/api/:path*', // 所有 API 路由
'/dashboard/:path*', // 所有 dashboard 页面
'/((?!_next|static|favicon.ico).*)', // 排除静态资源
],
}
(2) 常见 Middleware 模式
// middleware.ts — 常见模式汇总
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 1. 认证检查
const token = request.cookies.get('session')?.value
const isAuthPage = pathname.startsWith('/login') || pathname.startsWith('/register')
if (!token && !isAuthPage && pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
// 2. 安全头
const response = NextResponse.next()
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
// 3. 添加 request ID(追踪)
const requestId = crypto.randomUUID()
response.headers.set('X-Request-Id', requestId)
return response
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
}
▶ 示例:Middleware 日志记录(难度⭐)
// middleware.ts — 请求日志
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const start = Date.now()
const { method, nextUrl } = request
console.log(`[${new Date().toISOString()}] ${method} ${nextUrl.pathname}`)
const response = NextResponse.next()
// 异步记录响应时间
response.headers.set('X-Response-Time', `${Date.now() - start}ms`)
return response
}
export const config = {
matcher: '/api/:path*',
}
7. CORS 与 Rate Limiting
(1) CORS 配置
// app/api/cors-config/route.ts — 单个端点的 CORS
import { NextRequest, NextResponse } from 'next/server'
const corsHeaders = {
'Access-Control-Allow-Origin': 'https://your-app.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
}
export async function OPTIONS() {
return NextResponse.json({}, { headers: corsHeaders })
}
export async function GET() {
return NextResponse.json(
{ data: 'CORS enabled' },
{ headers: corsHeaders }
)
}
(2) 全局 CORS Middleware
// middleware.ts — 全局 CORS
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/api/')) {
const response = NextResponse.next()
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
// 预检请求直接返回
if (request.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: response.headers })
}
return response
}
return NextResponse.next()
}
export const config = {
matcher: '/api/:path*',
}
(3) Rate Limiting
// lib/rate-limit.ts — 简单的内存 Rate Limiter
const rateMap = new Map<string, { count: number; resetAt: number }>()
export function rateLimit(key: string, maxRequests: number, windowMs: number): boolean {
const now = Date.now()
const record = rateMap.get(key)
if (!record || now > record.resetAt) {
rateMap.set(key, { count: 1, resetAt: now + windowMs })
return true // 允许
}
if (record.count >= maxRequests) {
return false // 限制
}
record.count++
return true
}
// app/api/rate-limited/route.ts — 使用 Rate Limiter
import { NextRequest, NextResponse } from 'next/server'
import { rateLimit } from '@/lib/rate-limit'
export async function GET(request: NextRequest) {
const ip = request.headers.get('x-forwarded-for') ?? 'anonymous'
const allowed = rateLimit(ip, 10, 60_000) // 每分钟 10 次
if (!allowed) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429, headers: { 'Retry-After': '60' } }
)
}
return NextResponse.json({ message: 'Success', timestamp: Date.now() })
}
▶ 示例:完整 CORS + Rate Limiting(难度⭐⭐⭐)
// app/api/secure/route.ts — CORS + Rate Limit + Auth
import { NextRequest, NextResponse } from 'next/server'
import { rateLimit } from '@/lib/rate-limit'
import { z } from 'zod'
const postSchema = z.object({
title: z.string().min(2).max(200),
content: z.string().min(10),
})
export async function POST(request: NextRequest) {
// 1. Rate Limit
const ip = request.headers.get('x-forwarded-for') ?? 'unknown'
if (!rateLimit(ip, 20, 60_000)) {
return NextResponse.json({ error: 'Rate limit exceeded' }, { status: 429 })
}
// 2. Auth check
const auth = request.headers.get('authorization')
if (!auth?.startsWith('Bearer ') || auth.slice(7) !== process.env.API_KEY) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// 3. Validation
const body = await request.json()
const validated = postSchema.safeParse(body)
if (!validated.success) {
return NextResponse.json(
{ error: 'Validation failed', details: validated.error.flatten().fieldErrors },
{ status: 400 }
)
}
// 4. Process
const post = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: JSON.stringify({ ...validated.data, userId: 1 }),
}).then(r => r.json())
return NextResponse.json(post, { status: 201, headers: {
'Access-Control-Allow-Origin': '*',
}})
}
export async function OPTIONS() {
return NextResponse.json({}, { headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}})
}
8. 完整示例:RESTful API 服务
// app/api/posts/route.ts — Posts CRUD API
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { revalidateTag } from 'next/cache'
// ======== Schemas ========
const createPostSchema = z.object({
title: z.string().min(2).max(200),
body: z.string().min(10).max(5000),
userId: z.number().int().positive(),
})
const querySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(50).default(10),
})
const API = 'https://jsonplaceholder.typicode.com'
// ======== GET /api/posts?page=1&limit=10 ========
export async function GET(request: NextRequest) {
const query = querySchema.safeParse({
page: request.nextUrl.searchParams.get('page'),
limit: request.nextUrl.searchParams.get('limit'),
})
if (!query.success) {
return NextResponse.json(
{ error: 'Invalid query', details: query.error.flatten().fieldErrors },
{ status: 400 }
)
}
const { page, limit } = query.data
const posts = await fetch(`${API}/posts?_page=${page}&_limit=${limit}`).then(r => r.json())
const total = 100 // JSONPlaceholder 总量
return NextResponse.json({
data: posts,
pagination: { page, limit, total, totalPages: Math.ceil(total / limit) }
})
}
// ======== POST /api/posts ========
export async function POST(request: NextRequest) {
const body = await request.json()
const validated = createPostSchema.safeParse(body)
if (!validated.success) {
return NextResponse.json(
{ error: 'Validation failed', fields: validated.error.flatten().fieldErrors },
{ status: 400 }
)
}
const post = await fetch(`${API}/posts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(validated.data),
}).then(r => r.json())
revalidateTag('posts')
return NextResponse.json(post, { status: 201 })
}
// app/api/posts/[id]/route.ts — 单文章 CRUD
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
const updatePostSchema = z.object({
title: z.string().min(2).max(200).optional(),
body: z.string().min(10).max(5000).optional(),
})
const API = 'https://jsonplaceholder.typicode.com'
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = await fetch(`${API}/posts/${params.id}`).then(r => {
if (!r.ok) throw new Error('Not found')
return r.json()
}).catch(() => null)
if (!post) {
return NextResponse.json({ error: 'Post not found' }, { status: 404 })
}
return NextResponse.json(post)
}
export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const body = await request.json()
const validated = updatePostSchema.safeParse(body)
if (!validated.success) {
return NextResponse.json(
{ error: 'Validation failed', fields: validated.error.flatten().fieldErrors },
{ status: 400 }
)
}
const updated = await fetch(`${API}/posts/${params.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(validated.data),
}).then(r => r.json())
return NextResponse.json(updated)
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
await fetch(`${API}/posts/${params.id}`, { method: 'DELETE' })
return NextResponse.json({ success: true, id: params.id })
}
// middleware.ts — 全局 API 中间件
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { rateLimit } from '@/lib/rate-limit'
export function middleware(request: NextRequest) {
const { pathname, origin } = request.nextUrl
if (!pathname.startsWith('/api/')) {
return NextResponse.next()
}
// 1. Rate Limiting
const ip = request.headers.get('x-forwarded-for') ?? 'unknown'
if (!rateLimit(ip, 30, 60_000)) {
return NextResponse.json({ error: 'Too many requests' }, {
status: 429,
headers: { 'Retry-After': '60' }
})
}
// 2. Logging
console.log(`[API] ${request.method} ${pathname}`)
// 3. CORS
const response = NextResponse.next()
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key')
if (request.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: response.headers })
}
return response
}
export const config = {
matcher: '/api/:path*',
}
❓ 常见问题
route.ts 定义,适合第三方 API 集成、Webhook、移动端调用。Server Action 是 RPC 风格的服务端函数,通过 'use server' 定义,适合第一方表单操作。区别在于:Route Handler 需要 HTTP 调用,Server Action 是函数调用;Route Handler 无 CSRF 保护,Server Action 内置。export const runtime = 'edge' 切换到 Edge Runtime。Edge Runtime 延迟更低,但 API 受限(不支持 Node.js 原生模块如 fs、crypto)。Edge Runtime 适合简单代理、认证校验、A/B 测试。upstash/ratelimit)以支持多实例共享状态。request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() 或 request.headers.get('x-real-ip')。Vercel 部署时会自动添加这些头。本地开发时可能返回 ::1 或 127.0.0.1。📖 小节
route.ts文件通过导出 GET/POST/PUT/DELETE 等函数定义 RESTful 端点- Zod 校验请求体和查询参数,返回 400 状态码和详细错误信息
NextResponse支持 json()、redirect()、next()、rewrite() 四种响应格式- middleware.ts 在请求到达页面/API 前统一处理认证、日志、CORS
- matcher 配置控制 middleware 的路径匹配范围
- CORS 通过 Middleware 或 Route Handler 中的响应头配置
- Rate Limiting 使用内存 Map 或 Redis 实现(生产环境推荐 Upstash Ratelimit)
📝 作业
-
基础题(⭐):创建
app/api/health/route.ts返回 JSON 健康检查信息(状态、时间戳、uptime)。添加 middleware 记录每次 API 调用的日志和响应时间。 -
进阶题(⭐⭐):构建一个完整的 CRUD API:
app/api/books/route.ts(GET 列表 + POST 创建)+app/api/books/[id]/route.ts(GET 详情 + PUT 更新 + DELETE 删除)。使用 Zod 校验请求体。在 middleware 中添加 API Key 认证。 -
挑战题(⭐⭐⭐):实现一个"短链接服务"API:
app/api/shorten/route.ts(POST 接受 url 返回短码)、app/api/[code]/route.ts(GET 读取短码并 302 重定向)。添加 Rate Limiting(每分钟 10 个短链接/IP)、CORS 跨域支持、Middleware 访问日志。短码存储使用 JSON 文件或内存 Map。