本节摘要:全栈应用需要"后端接口"——被外部调用、或客户端组件获取数据。Next.js 用 route.ts 定义 API 路由(Route Handlers)。本节讲清 GET/POST 的写法、请求与响应处理、动态路由参数,以及"何时用 API 路由、何时用 Server Actions"的选择。
阅读完本节,你应当能够:
服务器组件能直接查库,为什么还需要 API 路由?因为有三类场景必须暴露 HTTP 接口:
直觉类比:API 路由是"对外开放的窗口"——服务器组件是"内部直连",API 路由是"门口柜台":外部来客(外部系统)只能通过柜台办事,柜台有权限控制、有台账(日志)。
💡 关键直觉:route.ts 就是"文件的 API 版本"——page.tsx 定义页面(返回 HTML),route.ts 定义接口(返回 JSON)。同样的文件系统路由约定,一个面向浏览器,一个面向程序。
// app/api/hello/route.ts → GET /api/hello import { NextResponse } from "next/server"; export async function GET() { return NextResponse.json({ message: "Hello API" }); }
URL 对应:app/api/hello/route.ts → /api/hello,与 page.tsx 同约定。
// app/api/users/route.ts import { NextRequest, NextResponse } from "next/server"; // GET /api/users?page=2&limit=10 export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const page = searchParams.get("page") ?? "1"; const limit = searchParams.get("limit") ?? "10"; return NextResponse.json({ page, limit }); } // POST /api/users body: {"name":"alice"} export async function POST(request: NextRequest) { const body = await request.json(); // 解析 JSON 体 const name = body.name; // ... 写数据库 return NextResponse.json( { id: 1, name }, { status: 201 } // 创建成功状态码 ); }
// app/api/users/[id]/route.ts → /api/users/42 import { NextRequest, NextResponse } from "next/server"; export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params; return NextResponse.json({ userId: id }); }
API 路由默认不缓存(每次请求都执行)。需要缓存可配置:
export const dynamic = "force-static"; // 构建时静态化 export const revalidate = 3600; // 每小时重新验证
function ok<T>(data: T) { return NextResponse.json({ code: 0, data }); } function fail(code: number, message: string, status = 400) { return NextResponse.json({ code, message }, { status }); } // 使用 export async function GET() { try { const users = await getUsers(); return ok(users); } catch (e) { return fail(500, "获取用户失败", 500); } }
规范:code(业务码,0 成功)+ data(数据)+ message(错误信息),前后端按此约定联调。
export async function POST(request: NextRequest) { // 1. 鉴权(第三章 3.2 详解) const session = await getSession(); if (!session) return NextResponse.json({ error: "未登录" }, { status: 401 }); // 2. 校验输入 const body = await request.json(); if (!body.name || body.name.length < 2) { return NextResponse.json({ error: "name 至少 2 字符" }, { status: 400 }); } // 3. 业务处理 return NextResponse.json({ ok: true }, { status: 201 }); }
| 场景 | 选择 |
|---|---|
| 表单提交(页面内) | Server Actions(2.8) |
| 客户端组件取数 | 两者皆可(优先服务器组件转发) |
| 外部系统调用 | API 路由 |
| Webhook 回调 | API 路由 |
| 文件上传 | API 路由(表单处理更灵活) |
| 数据变更(增删改) | Server Actions(表单场景) |
核心判断:面向"页面交互"用 Server Actions(更简洁),面向"外部程序"用 API 路由(更标准)。
| 误区 | 现象 | 正解 |
|---|---|---|
| 路由文件命名错 | 404 | 必须是 route.ts |
| 与 page.tsx 同目录 | 冲突报错 | 一个目录不能同时有 page 和 route(app/api 例外) |
| body 解析失败 | body 为 undefined | await request.json() |
| 返回 404 | 路径不对 | 检查目录结构对应 URL |
| 忘记处理 OPTIONS | 跨域报错 | 需要 CORS 时实现 OPTIONS 或配置 headers |
// app/api/posts/route.ts —— 列表 + 创建 import { NextRequest, NextResponse } from "next/server"; const posts = []; // 演示用内存存储,实际用数据库 export async function GET() { return NextResponse.json(posts); } export async function POST(request: NextRequest) { const body = await request.json(); const post = { id: posts.length + 1, title: body.title, body: body.body }; posts.push(post); return NextResponse.json(post, { status: 201 }); }
// app/api/posts/[id]/route.ts —— 详情 + 删除 export async function GET( _req: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params; const post = posts.find((p) => p.id === Number(id)); if (!post) return NextResponse.json({ error: "不存在" }, { status: 404 }); return NextResponse.json(post); } export async function DELETE( _req: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params; const idx = posts.findIndex((p) => p.id === Number(id)); if (idx === -1) return NextResponse.json({ error: "不存在" }, { status: 404 }); posts.splice(idx, 1); return NextResponse.json({ ok: true }); }
用 curl 测试:`curl 「相关地址请参见官方文档」 -X POST -H "Content-Type: application/json" -d '{"title":"hi"}' 「相关地址请参见官方文档」 app/api 目录里,与前端同项目同部署**。
处理顺序:鉴权 → 校验 → 业务 → 响应。任何一步失败都要返回明确的状态码与错误信息,让调用方知道"错在哪"。
流式响应(适合大文件下载、SSE 推送):
export async function GET() { const stream = await getLargeData(); return new Response(stream, { headers: { "Content-Type": "text/event-stream" }, }); }
文件上传:
export async function POST(request: NextRequest) { const formData = await request.formData(); const file = formData.get("file") as File; // 校验类型与大小后保存 return NextResponse.json({ name: file.name, size: file.size }); }
API 路由的缓存心智:默认不缓存(每次执行)——适合个性化数据;纯读接口可配 revalidate 缓存。写接口(POST/PUT/DELETE)永远不缓存,这是默认行为也符合直觉。
问:API 路由和 Server Actions 能混用吗?
能,且常见。规则:页面内表单用 Server Actions,外部程序/移动端/Webhook 用 API 路由。两者可调用同一套业务函数,避免逻辑重复。
问:API 路由的返回能是 HTML 吗?
能。new Response(html, { headers: { "Content-Type": "text/html" } }) 或直接返回字符串。但页面用 page.tsx,API 路由一般只返回 JSON。
问:如何保护 API 路由?
每次请求检查认证(auth() 读 session),校验输入,设置 CORS(跨域场景)。与 Server Action 一样,API 路由是网络接口,必须自己鉴权。
问:API 路由会自动生成文档吗?
不会。需要文档时用 OpenAPI 工具(如 next-swagger-doc)或在代码注释中维护。简单项目用 README 维护接口清单即可。
Route Handlers 是"文件的 API 版本"——route.ts 定义接口,与 page.tsx 同路由约定。核心纪律:接口必须自己鉴权与校验,统一返回结构(code/message/data),页面交互用 Server Actions、外部程序用 API 路由。掌握"何时用哪个",就掌握了 Next.js 的后端能力拼图。
做一个"待办 API"来验证本节知识:用 route.ts 实现 GET(列表)、POST(创建)、DELETE(删除)三个接口,用 curl 分别测试成功与错误场景(参数缺失、资源不存在)。再给接口加上简单鉴权(读一个硬编码 token 头),体会"API 路由必须自己保护"。
进阶练习:把列表接口改成支持 revalidate 缓存(export const revalidate = 60),观察缓存行为;再用 NextResponse.json 统一错误结构(code/message),让调用方容易处理。做完这些,你就掌握了"全栈应用的后端"在 Next.js 中的完整形态。
试着不看书回答:route.ts 里 GET 函数返回什么?如何读取路径参数与查询参数?API 路由与 Server Actions 的分工是什么?答对即掌握本节核心。
记住:接口写得好不好,看调用方是否不用猜——状态码语义化、错误信息明确、参数有校验。API 是契约,契约清晰,前后端协作才顺畅。调试接口时先确认三个问题:URL 对不对、方法对不对、参数格式对不对——九成 404/400 都出在这三处。