跳到主要内容

Next.js 写操作与接口层

在 App Router 里,Route Handlers 基本就是接口层正路。

如果只把它看成“API Routes 换了个名字”,很容易低估它现在和缓存、鉴权、重验证之间的关系。更贴切一点的理解是:

  • 它延续了文件系统路由
  • 直接使用 Web Request / Response API
  • 可以和 App Router 的缓存、重验证、鉴权体系接起来
  • 很适合承担 BFF 和轻量服务端接口

Server Actions 先怎么理解

Server Actions 是 App Router 里非常关键的一块能力。它让“写操作”不再一定要先绕到单独接口层,再从客户端手工拼一次请求。

先把它理解成什么

可以把 Server Actions 理解成:

  • 定义在服务端的可调用函数
  • 常用于表单提交和数据写入
  • 可以直接接上重定向、重验证和错误处理

它不是要彻底取代 Route Handlers,而是给页面级写操作提供更顺手的一条主线。

一个最小例子

'use server';

import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
const title = String(formData.get('title'));

await db.post.create({ title });
revalidatePath('/posts');
}

然后在组件里直接使用:

import { createPost } from './actions';

export default function CreatePostForm() {
return (
<form action={createPost}>
<input name="title" />
<button type="submit">Create</button>
</form>
);
}

它为什么会比传统接口更顺

传统写法通常是:

  1. 前端提交表单
  2. 请求 /api/*
  3. 接口写数据库
  4. 前端再手工刷新列表或跳转

Server Actions 可以把这条链压得更短:

  1. 表单直接调用服务端函数
  2. 服务端写入
  3. 服务端立即 revalidatePath / revalidateTag
  4. 页面结果自动更新或再导航

什么时候适合用

很适合:

  • 表单提交
  • 后台管理写操作
  • 设置页更新
  • 点赞、收藏、评论、增删改这类页面内动作

不一定适合:

  • 复杂公共 API
  • 给第三方系统调用的接口
  • 需要独立开放给外部客户端的后端能力

这类场景通常还是 Route Handlers 或独立服务更稳。

参数和返回值怎么理解

Server Action 常见参数包括:

  • FormData
  • 普通序列化参数

例如:

'use server';

export async function updateStatus(id: string, status: 'draft' | 'published') {
await db.post.update(id, { status });
}

在客户端组件里:

'use client';

import { updateStatus } from './actions';

export function PublishButton({ id }: { id: string }) {
return (
<button onClick={() => updateStatus(id, 'published')}>
Publish
</button>
);
}

常和哪些能力一起出现

revalidatePath

写完数据后,刷新某个页面路径。

revalidateTag

写完数据后,按数据标签失效。

redirect

写完数据后直接跳转。

'use server';

import { redirect } from 'next/navigation';

export async function createInvoice(formData: FormData) {
await db.invoice.create({
title: String(formData.get('title')),
});

redirect('/invoices');
}

useActionState

适合把提交结果和错误信息回传给 UI。

错误处理怎么做

常见思路有两种:

1. 返回结构化结果

'use server';

export async function createUser(_: unknown, formData: FormData) {
const email = String(formData.get('email'));

if (!email) {
return { error: 'email is required' };
}

await db.user.create({ email });
return { success: true };
}

2. 抛异常,交给错误边界处理

适合更全局的失败情况。

安全边界要注意什么

1. Server Action 在服务端执行,不代表自动安全

鉴权、授权、输入校验仍然都要做。

2. 不要把只有客户端才能决定的业务假设直接信任

例如用户角色、价格、可编辑字段等,都应该在服务端再次确认。

3. 写操作后一定要想清楚缓存失效

否则最常见的结果就是:写入成功了,但页面还是旧数据。

Route Handlers 和 Server Actions 怎么选

更适合 Server Actions

  • 页面内写操作
  • 表单驱动流程
  • 只服务当前站点 UI

更适合 Route Handlers

  • 标准 REST / JSON API
  • Webhook
  • 给移动端、小程序、第三方客户端调用
  • 需要独立协议边界的服务端接口

常见误区

1. 以为有了 Server Actions 就再也不需要接口

不是。两者解决的问题不完全一样。

2. 忘记做重验证

这是最容易踩的坑。数据写进去了,但读缓存没失效,页面看起来像没更新。

3. 把大量复杂服务端逻辑都堆进 action 文件

Action 更适合做流程入口。重逻辑最好继续沉到 lib/ 或 service 层。

推荐继续往下看

  1. 缓存与重验证
  2. 升级检查清单

参考资料

Route Handlers 再怎么理解

它和 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 方法:

  • GET
  • POST
  • PUT
  • PATCH
  • DELETE
  • HEAD
  • OPTIONS

如果没实现某个方法,Next 会返回 405 Method Not Allowed

它和 page.tsx 的关系

一个很重要的限制是:

  • 同一个路由段层级,不能同时存在 page.tsxroute.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 / ResponseNextRequest / NextResponse

Route Handlers 默认支持标准 Web API:

  • Request
  • Response

同时 Next 也提供了:

  • NextRequest
  • NextResponse

它们主要用于:

  • 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 的写操作
  • 提交后要紧接着 redirectrevalidatePath

如果需求天然是“给页面自己用”,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 在服务端执行,不代表输入天然可信。

比较常见的做法是:

  • zod
  • valibot
  • 手写 schema 校验

如果你想把 zod 这条线系统补一遍,可以接着看 运行时校验与 Zod

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。

中间层最好别做成什么样

1. 所有业务都塞进 route.ts

route.ts 适合做协议入口,不适合承载所有业务细节。复杂逻辑最好沉到 lib/ 或 service 层。

2. 完全不考虑缓存语义

读接口是否缓存、缓存多久,会直接影响前端体感和服务端压力。

3. 只做登录检查,不做资源级授权

“已登录”不等于“能访问这条资源”。

推荐继续往下看

  1. 鉴权、权限与 Proxy
  2. 缓存与重验证
  3. 部署、自托管与 standalone

参考资料