Next.js Route Handlers 与 API 设计
在 App Router 里,Route Handlers 基本就是接口层正路。
如果只把它看成“API Routes 换了个名字”,很 容易低估它现在和缓存、鉴权、重验证之间的关系。更贴切一点的理解是:
- 它延续了文件系统路由
- 直接使用 Web
Request/ResponseAPI - 可以和 App Router 的缓存、重验证、鉴权体系接起来
- 很适合承担 BFF 和轻量服务端接口
它和 Pages Router 里的 API Routes 是什么关系
官方文档说得很清楚:
- Route Handlers 只存在于
app/目录 - 它们是
pages/api/*的对应方案 - 没必要为了同一类需求同时混用两套接口层
所以在新项目里,通常优先直接按 Route Handlers 理解。
最基本的文件约定
app/api/posts/route.ts
export async function GET(request: Request) {
return Response.json({ ok: true });
}
它支持这些 HTTP 方法:
GETPOSTPUTPATCHDELETEHEADOPTIONS
如果没实现某个方法,Next 会返回 405 Method Not Allowed。
它和 page.tsx 的关系
一个很重要的限制是:
- 同一个路由段层级,不能同时存在
page.tsx和route.ts
也就是说:
page负责页面route负责接口- 两者都是底层路由原语,但职责不同
为什么它很适合做 BFF
Route Handlers 很适合这些场景:
- 聚合多个后端接口
- 清洗第三方 API 数据
- 注入鉴权上下文
- 给前端屏蔽数据库或服务端细节
- 处理 webhook 或回调
例如:
import { NextResponse } from 'next/server';
export async function GET() {
const [profile, orders] = await Promise.all([
getProfile(),
getOrders(),
]);
return NextResponse.json({ profile, orders });
}
这类场景不一定需要单独起一个完整后端服务,但又比直接在客户端拼多个请求更合适放在服务端。
Request / Response 和 NextRequest / NextResponse
Route Handlers 默认支持标准 Web API:
RequestResponse
同时 Next 也提供了:
NextRequestNextResponse
它们主要用于:
- cookie 读取
- URL 辅助处理
- redirect / rewrite
- 一些 Next 运行时相关能力
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
const search = request.nextUrl.searchParams.get('q');
return NextResponse.json({ search });
}
读取参数的几种方式
查询参数
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = searchParams.get('page');
return Response.json({ page });
}
动态路由参数
app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
return Response.json({ id });
}
较新的版本里,params 也要按异步模型理解。
请求体怎么读
JSON
export async function POST(request: Request) {
const body = await request.json();
return Response.json({ received: body });
}
FormData
export async function POST(request: Request) {
const formData = await request.formData();
const title = formData.get('title');
return Response.json({ title });
}
文本
export async function POST(request: Request) {
const raw = await request.text();
return new Response(raw);
}
Route Handlers 的缓存怎么理解
官方当前文档写得很明确:
- Route Handlers 默认不缓存
- 只有
GET可以选择显式进入缓存 - 其他方法默认不缓存
例如:
export const dynamic = 'force-static';
export async function GET() {
const products = await getProducts();
return Response.json(products);
}
如果走当前 cacheComponents 路线,也可以把缓存逻辑下沉到 helper:
import { cacheLife } from 'next/cache';
export async function GET() {
const products = await getProducts();
return Response.json(products);
}
async function getProducts() {
'use cache';
cacheLife('hours');
return db.product.findMany();
}
一个很实用的判断是:
- 读接口先考虑需不需要缓存
- 写接口默认按实时和安全优先处理
和 Server Actions 怎么选
这两个经常会被拿来比较。
更适合 Route Handlers
- 标准 REST / JSON API
- webhook
- 第三方回调
- 移动端、小程序、外部系统也要调用
- 需要明确协议边界
更适合 Server Actions
- 页面内表单提交
- 只服务当前站点 UI 的写操作
- 提交后要紧接着
redirect或revalidatePath
如果需求天然是“给页面自己用”,Server Actions 往往更顺。 如果需求是“接口本身就是产品边界的一部分”,Route Handlers 更合适。
API 设计怎么写得更稳
1. 路由命名尽量按资源组织
例如:
app/api/posts/route.ts
app/api/posts/[id]/route.ts
app/api/users/[id]/settings/route.ts
这样可读性通常比把动词堆进路径里更好。
2. 响应结构尽量稳定
例如统一:
return Response.json({ data, error: null });
或者:
return Response.json({ message: 'not found' }, { status: 404 });
最重要的不是某种格式绝对最好,而是全项目保持一致。
3. 错误码和业务错误分开
401:未登录403:已登录但无权限404:资源不存在422:参数语义错误500:服务端异常
这比一律返回 200 + success: false 更适合作为长期接口契约。
4. 参数校验不要省
Route Handlers 在服务端执行,不代表输入天然可信。
比较常见的做法是:
zodvalibot- 手写 schema 校验
import { z } from 'zod';
const createPostSchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
});
export async function POST(request: Request) {
const body = await request.json();
const parsed = createPostSchema.safeParse(body);
if (!parsed.success) {
return Response.json({ error: parsed.error.flatten() }, { status: 422 });
}
return Response.json({ ok: true });
}
5. 鉴权和授权贴近数据层再做一次
即使前面有 proxy.ts,Route Handler 自己也应该再做一次权限检查。
最常见的几类接口
1. 读列表
export async function GET() {
const posts = await db.post.findMany();
return Response.json(posts);
}
2. 写入资源
export async function POST(request: Request) {
const body = await request.json();
const post = await db.post.create(body);
return Response.json(post, { status: 201 });
}
3. webhook
export async function POST(request: Request) {
const signature = request.headers.get('x-signature');
const rawBody = await request.text();
await verifyWebhook(signature, rawBody);
return Response.json({ received: true });
}
webhook 这类场景常常更适合直接 Route Handler,而不是 Server Actions。