CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
Route Handlers
这一课你将能做到
在 app/api/**/route.ts 里写出 GET / POST 等接口,处理查询参数、JSON 请求体与动态段
实现一个「转发到 Spring Boot 并透传登录态」的 BFF 接口(本站 roadmap-node 接口的同款模式)
说清 Route Handler 与 Server Action 的区别,知道各自该用在什么场景
为什么要学
App Router 出现之前,Next.js 项目的后端接口要么另起一个 Express/Koa 服务,要么全部依赖外部后端。Route Handlers 把「写接口」变成了在 app/ 目录里建一个 route.ts 文件——和页面放在一起,同一套部署。
什么时候真的需要它?三个高频场景:
BFF(Backend for Frontend) :前端不想暴露后端真实地址,或需要在中间「加一层」(拼数据、鉴权、限流、改格式)。本站就有一个活例子:src/app/api/roadmap-node/[slug]/[nodeKey]/route.ts,它把请求转发到后端并透传用户 token。
给第三方提供回调入口 :支付回调、Webhook 接收,必须是一个公网 URL。
纯小项目 :个人工具、demo、内部系统,不想为几个接口起一个 Java 服务。
面试角度,「Route Handler 是什么、和 API Routes 有何区别、和 Server Action 怎么选」属于 App Router 新特性题的常客。答案核心:Route Handler 是 HTTP 层(有 URL、能收任意 HTTP 请求),Server Action 是 RPC 层(表单/组件直接调函数,无独立 URL 语义)。
先建立图像
Route Handler 的文件约定一图流:
app/api/health/route.ts → GET /api/health
app/api/echo/route.ts → POST /api/echo
app/api/tracks/[slug]/route.ts → GET /api/tracks/java-essentials
route.ts 里导出的函数名 = 允许的 HTTP 方法:
export async function GET() → 处理 GET
export async function POST() → 处理 POST
还有 PUT / PATCH / DELETE / HEAD / OPTIONS
两条铁律:
文件名必须 叫 route.ts(不是 page.tsx,也不是 index.ts)
同一文件夹里 route.ts 和 page.tsx 不能共存(同一个 URL 冲突)
请求与响应的最小骨架:
Request(入) Response(出)
┌──────────────────┐ ┌──────────────────────┐
│ request.nextUrl │ 查询参数 │ NextResponse.json() │ JSON 响应
│ request.headers │ 请求头 │ NextResponse.redirect│ 跳转
│ await request.json() 请求体 │ new Response(body) │ 自定义
│ { params } │ 动态段 │ NextResponse.cookies │ 操作 Cookie
└──────────────────┘ └──────────────────────┘
跟着做
步骤 1:第一个 GET 接口——健康检查
新建 src/app/api/health/route.ts:
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({
status: "ok",
time: new Date().toISOString(),
});
}
启动 dev server 后浏览器访问 http://localhost:3000/api/health,看到 JSON。恭喜,你写了第一个跑在 Next.js 里的后端接口。
步骤 2:查询参数与 POST 请求体 src/app/api/echo/route.ts——把「你发什么」原样诊断回来:
import { NextRequest, NextResponse } from "next/server";
// GET /api/echo?q=hello&page=2
export async function GET(request: NextRequest) {
const q = request.nextUrl.searchParams.get("q");
const page = request.nextUrl.searchParams.get("page") ?? "1";
return NextResponse.json({ q, page });
}
// POST /api/echo,body: { "name": "tom" }
export async function POST(request: NextRequest) {
const body = await request.json();
const name = body?.name ?? "anonymous";
return NextResponse.json({ message: `你好,${name}`, receivedAt: Date.now() });
}
curl "http://localhost:3000/api/echo?q=next&page=3"
curl -X POST http://localhost:3000/api/echo \
-H "Content-Type: application/json" \
-d '{"name":"tom"}'
要点:查询参数用 request.nextUrl.searchParams;请求体必须 await request.json()(读取是异步的)。参数校验在生产里建议上 zod 之类的库,先手写判断也行。
步骤 3:动态段接口 src/app/api/tracks/[slug]/route.ts:
import { NextResponse } from "next/server";
const DB: Record<string, { slug: string; title: string }> = {
"java-essentials": { slug: "java-essentials", title: "Java 基础" },
"nextjs-essentials": { slug: "nextjs-essentials", title: "Next.js 基础" },
};
export async function GET(
_request: Request,
{ params }: { params: Promise<{ slug: string }> }
) {
const { slug } = await params; // Next 15:params 是 Promise
const track = DB[slug];
if (!track) {
return NextResponse.json({ error: "课程不存在" }, { status: 404 });
}
return NextResponse.json(track);
}
访问 /api/tracks/java-essentials 得到数据,访问不存在的 slug 得到 404。注意第二个函数参数的位置——params 在第二个参数 里,和页面组件一致(同样 await)。
步骤 4:BFF 转发接口——本站同款模式 场景:浏览器不该直接知道 Spring Boot 的地址,且后端需要用户 token 做鉴权。写一个代理层:
// src/app/api/progress/route.ts
import { NextRequest, NextResponse } from "next/server";
const BACKEND = process.env.API_INTERNAL_URL ?? "http://localhost:8080";
export async function GET(request: NextRequest) {
// 1. 透传登录态:把浏览器带来的 token 转给后端
const token = request.cookies.get("token")?.value;
const search = request.nextUrl.search; // 保留查询参数
// 2. 服务端到服务端转发(走内网地址)
const res = await fetch(`${BACKEND}/api/progress${search}`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
cache: "no-store", // 用户数据绝不缓存
});
// 3. 原样回传状态码与内容
const data = await res.json().catch(() => ({}));
return NextResponse.json(data, { status: res.status });
}
前端调用时只知道自己域名下的 /api/progress,后端地址、内网拓扑、token 机制全部封装在服务端。本站 platform 的 roadmap-node 接口就是这一模式 (读 cookie 里的登录态 → fetch 后端 → 回传),第 11 课会带读源码对上号。
步骤 5:写 Cookie 的响应 登录场景常见需求——服务端把 token 写进 HttpOnly Cookie:
// src/app/api/login/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const { username, password } = await request.json();
// 真实项目:转发给后端校验,这里演示 Cookie 写法
if (username === "tom" && password === "123456") {
const res = NextResponse.json({ ok: true, username });
res.cookies.set("token", "demo-jwt-token", {
httpOnly: true, // JS 读不到,防 XSS 窃取
sameSite: "lax", // 基础 CSRF 防护
maxAge: 60 * 60 * 24 * 7,
path: "/",
secure: process.env.NODE_ENV === "production", // 生产必须 HTTPS
});
return res;
}
return NextResponse.json({ ok: false, message: "用户名或密码错误" }, { status: 401 });
}
httpOnly + secure + sameSite 三个属性是 Cookie 安全的标配,第 10 课认证会继续展开。
讲清楚要点
Route Handler 是 HTTP 层 :有 URL、有方法、有状态码,一切按 Web 标准来——Webhook、回调、公开 API、BFF 转发都找它。它接收 Web 标准 Request(Next 扩展为 NextRequest),返回 Response/NextResponse。
默认缓存语义与页面一致 :用 GET 且无动态输入的 handler 在 Next 14 会被静态化(启动时执行一次);Next 15 起默认动态。需要强制动态可用 export const dynamic = "force-dynamic",或在读 request 对象后自然变动态。POST 等写方法永远动态。
和 Server Action 的分工 (面试重点):Server Action 是「组件里直接调的服务端函数」,没有独立 URL,适合表单提交、页面内变更(如点赞写库);Route Handler 是「真接口」,适合回调、第三方调用、跨端复用(App 与 Web 共用)、需要 GET 语义的读取。本站两条都有:页面交互走 Server 组件取数,roadmap 节点走 Route Handler 转发。
BFF 的价值不只是藏地址 :聚合(一次请求拼多个后端接口)、协议转换(把 SOAP/XML 转成 JSON)、统一鉴权(token 检查集中一处)、降级(后端挂了返回友好兜底)。企业里前端团队对接口的掌控力就来自这一层。
错误返回要有语义 :参数错 400、未登录 401、无权限 403、不存在 404、服务器错 500,NextResponse.json({ error }, { status }) 是标准姿势。永远别吞错误返回 200——前端没法处理。
你可能会懵的地方
route.ts 里的 GET 能被别人直接猜到 URL 调用吗?
公网可达就是公开的。防滥用靠:鉴权头/Cookie 校验、限流(上游 Nginx 或代码内计数)、CORS 配置(非本站域名的浏览器请求默认被浏览器拦截,但 curl 不受 CORS 限制,所以服务端校验不能省)。
NextResponse.json 和 new Response(JSON.stringify(...)) 有区别吗?
结果等价,前者帮你设好了 Content-Type: application/json 和序列化,还链式提供 cookies 操作。永远优先 NextResponse。
为什么我的 GET handler 在 build 时就执行了?
Next 14 会静态化「无动态输入的 GET」。不想这样:读一下 request.nextUrl(用了 request 即动态)或导出 export const dynamic = "force-dynamic"。Next 15 已改为默认动态,但显式声明仍是好习惯。
Server Action 能替代所有 Route Handler 吗?
不能。Webhook 回调(GitHub、支付)必须有公网 URL;GET 语义的接口(分享链接、RSS、App 端调用)也不适合 Action。Action 的甜区是「页面内的写操作」。
handler 里能读 cookies() 吗?
能。import { cookies } from "next/headers" 后 await cookies(),效果同 request.cookies。用 request 参数更直观,团队统一一种即可。
错法对照 错法 问题 正解 接口文件命名为 index.ts 或 api.ts 不被识别为接口 文件名必须是 route.ts 同目录同时放 page.tsx 和 route.ts 路由冲突报错 接口与页面分目录:/api/x 与 /x 用 request.query 取查询参数 Request 对象没有这个属性 request.nextUrl.searchParams.get("q")所有错误都 return NextResponse.json({msg})(200) 前端无法按状态码分支处理 正确 status:400/401/404/500 BFF 转发时把 token 写死或硬编码后端地址 泄密 & 环境不可移植 从请求头/Cookie 透传 + 环境变量配置
卡点急救
405 Method Not Allowed :URL 对但方法没导出。文件里只写了 GET 却发 POST 就会 405——把对应方法的导出函数补上。
POST 收到的 body 是 undefined :忘了 await request.json(),或客户端没设 Content-Type: application/json(fetch 默认 text/plain,服务端 json() 解析失败)。
转发后端时报 CORS 错 :CORS 是浏览器行为,服务端到服务端的 fetch 不受影响。若报错出现在浏览器直连后端,要么后端配 CORS,要么让请求走你的 BFF 转发(这正是 BFF 的用途之一)。
面试常问
Route Handler 和旧版 API Routes 的区别?
API Routes 属于 Pages Router(pages/api/*.ts),路由与处理函数靠默认导出对象;Route Handlers 属于 App Router(app/**/route.ts),按导出函数名映射 HTTP 方法,直接使用 Web 标准 Request/Response,支持流式响应、与页面共享同一路由树和缓存体系。新项目一律 Route Handler。
Route Handler 和 Server Action 怎么选?
需要公网 URL、被非浏览器调用方(Webhook、App)、GET 可分享语义、BFF 聚合转发 → Route Handler。页面内表单提交、按钮触发的服务端变更、无需独立 URL 的内部调用 → Server Action(代码量更少,自动绑定表单渐进增强)。
说一个你写过的 BFF 接口,它解决了什么问题?
参考回答:学习平台有一个 /api/roadmap-node/[slug]/[nodeKey] 接口,服务端读取用户 Cookie 中的 token 后转发到内网 Spring Boot 服务,浏览器只感知同域接口。它同时解决了三件事:隐藏后端拓扑(内网地址不暴露)、集中鉴权(token 校验一处完成)、避免跨域(同源调用)。
自测三问
route.ts 导出哪些函数名可以处理哪些 HTTP 方法?动态段参数从哪拿?
BFF 转发接口里,token 从哪里取、转发到哪个地址、为什么不缓存?
什么需求必须用 Route Handler 而不能用 Server Action?
小结一句话 route.ts 让 Next.js 拥有了 HTTP 层:方法即导出名、params 在第二个参数、NextResponse 管输出;BFF 转发是它在企业项目里最实用的形态。
延伸