Skip to content
工程与部署
Next.js 关键 API:路由处理程序、中间件与 Server Actions
Next.js 关键 API:路由处理程序、中间件与 Server Actions 的概念、用法、示例和注意点
2024/01/195 分钟工程与部署
路由处理程序:服务端的 API 端点
在 app 目录下,除了 page.tsx 这种 UI 路由,还有一个同等权重的文件约定:route.ts。它不渲染界面,只负责响应 HTTP 请求。Next.js 把它叫作 Route Handler——本质上就是一个运行在服务端的 API 端点。
ts
// app/api/hello/route.ts
export async function GET() {
return Response.json({ message: 'hello' })
}访问 /api/hello,响应体就是 {"message":"hello"}。GET 是导出的异步函数,名字对应 HTTP 方法。POST、PUT、PATCH、DELETE、HEAD、OPTIONS 都可以在同一个文件里导出同名函数来声明。
注意:
route.ts与page.tsx不能共存于同一个路由段。一个文件夹要么是 UI 路由,要么是 API 路由。
一个处理动态参数的完整文件大致是这样:
ts
// app/api/posts/[id]/route.ts
export async function GET(
_req: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
// 根据 id 查询数据...
return Response.json({ id, title: 'post title' })
}
export async function DELETE(
_req: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
// 删除 id 对应的资源...
return Response.json({ success: true })
}第二个参数的 params 携带动态路由参数。在 Next.js 15 里 params 是 Promise,使用时需要 await——这是为了兼容未来的部分预渲染能力,在此之前同步解构也能工作,但类型上已经标记为异步。
从请求里提取数据
GET、DELETE 主要依赖路径参数和查询字符串。查询字符串从 request.url 里用标准 URL API 解析:
ts
export async function GET(req: Request) {
const url = new URL(req.url)
const page = url.searchParams.get('page') ?? '1'
const limit = url.searchParams.get('limit') ?? '10'
// ...
}POST 或 PUT 通常携带 JSON 或表单数据。读取 JSON 用 request.json():
ts
export async function POST(req: Request) {
const body = await req.json() as { title: string }
if (!body.title) {
return Response.json({ error: 'title 不能为空' }, { status: 400 })
}
// 处理写入...
return Response.json({ id: 1, ...body }, { status: 201 })
}读取 multipart/form-data 用 request.formData():
ts
const formData = await req.formData()
const file = formData.get('file') as File | null这些全部来自标准 Web Request。Next.js 额外提供了 NextRequest 扩展,通过 nextUrl 直接拿到解析好的 URL,用 cookies 和 headers 属性读写 Cookie 和请求头。如果只需做数据读写,原生 Request 足够。
返回响应
Route Handler 返回的是标准 Response,也可以用 NextResponse 提供带状态码的 JSON:
ts
import { NextResponse } from 'next/server'
export async function POST(req: Request) {
const body = await req.json()
if (!body.title?.trim()) {
return NextResponse.json(
{ error: 'title is required' },
{ status: 422 }
)
}
return NextResponse.json(
{ id: 1, title: body.title },
{ status: 201 }
)
}重定向用 NextResponse.redirect(new URL('/target', req.url)),它内部会设置 302 状态码和 Location 头。
中间件:请求到达前的拦截
Route Handler 能提供 API 端点,但鉴权逻辑如果散落在每个 handler 里会很难维护。中间件就是用来集中处理这类横切关注点的——它在请求匹配完成后、路由处理程序或页面渲染之前执行。
middleware.ts 通常放在项目根目录或 src 下,不能在 app 目录里。
基本结构:
ts
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// 改写请求、检查鉴权、添加响应头...
return NextResponse.next() // 放行
}NextResponse.next() 让请求继续流转。如果不调用它,或者返回一个新的 Response(比如重定向、401),请求便在此终止,不会到达目标路由。
matcher 与执行顺序
中间件默认匹配所有路由。通常只希望对特定路径生效,这时用 config.matcher 声明:
ts
export const config = {
matcher: ['/api/protected/:path*', '/dashboard/:path*'],
}matcher 的语法类似文件路径 glob,:path* 匹配零个或多个路径段。Next.js 暂不支持分层级中间件,只有一个 middleware.ts 能导出决策逻辑。不同路径的条件判断只能在函数体里通过 request.nextUrl.pathname 做分支。
执行顺序上,中间件先于 Route Handler 和页面渲染执行,没有例外。
鉴权示例
判断请求是否有权限,通常检查 Cookie 或 Authorization 头:
ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value
const authHeader = request.headers.get('authorization')
if (!token && !authHeader) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
// 如果有 token,可进一步解析 JWT 或查 session
return NextResponse.next()
}
export const config = {
matcher: ['/api/protected/:path*', '/dashboard/:path*'],
}到达 /api/protected/* 或 /dashboard/* 的请求会先经过这个中间件,没有凭据就直接返回 401。
中间件运行在 Edge Runtime 上,无法直接访问 Node.js API(文件系统、进程内存、大多数数据库驱动)。它适合做轻量判断——token 存在性检查、路由级重定向。完整的权限校验(查数据库 session、判断角色)应当放到 Route Handler 或 Server Action 里再执行一次。
Server Actions:表单与写操作
Route Handler 适合作为外部可访问的 API 端点,但如果写入只服务于页面自身的表单,Server Actions 是更贴合 React 模型的选择。
Server Action 是运行在服务端的异步函数,用 'use server' 声明。客户端可以直接调用,不需要手动发 fetch、建 Route Handler、处理序列化。
声明与调用
两种声明方式。一种是服务端组件内联:
tsx
// app/page.tsx (Server Component)
export default function Page() {
async function create(formData: FormData) {
'use server'
// 读写数据库、文件系统
}
return (
<form action={create}>
<input name="content" />
<button type="submit">提交</button>
</form>
)
}另一种是集中到一个文件,顶部写 'use server':
ts
// app/actions.ts
'use server'
export async function createMessage(formData: FormData) {
// ...
}
export async function updatePost(id: string, data: FormData) {
// ...
}导出的函数可以在客户端或服务端组件里直接 import。表单的 action 属性指向函数名,表单数据会作为 FormData 参数传入。单独处理某个按钮用 <button formAction={serverActionFn}>:
tsx
// 客户端组件
'use client'
import { createMessage } from './actions'
export function Form() {
return (
<form action={createMessage}>
<input name="content" />
<button formAction={createMessage}>发送</button>
</form>
)
}调用时 Next.js 内部会发一次类似 POST 的请求到服务端,执行函数体,再把结果传回客户端。整个过程对开发者透明,但本质上是异步操作。如果不用状态管理,用户可能重复点击或看不到反馈。
表单状态、校验与缓存刷新
React 的 useActionState 用来管理 Server Action 的返回值、pending 状态和错误信息。它接收 action 函数和初始状态,返回 [state, formAction, isPending]:
tsx
'use client'
import { useActionState } from 'react'
import { createMessage } from './actions'
export function MessageForm() {
const [state, formAction, pending] = useActionState(createMessage, {
error: null as string | null,
})
return (
<form action={formAction}>
<textarea name="content" />
{state?.error && <p className="error">{state.error}</p>}
<button type="submit" disabled={pending}>
{pending ? '提交中...' : '提交'}
</button>
</form>
)
}服务端的 createMessage 根据校验结果返回状态:
ts
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createMessage(
_prevState: { error: string | null },
formData: FormData
) {
const content = String(formData.get('content') ?? '')
if (!content.trim()) {
return { error: '内容不能为空' }
}
// 写入数据库...
// await db.message.create({ content })
revalidatePath('/guestbook')
redirect('/guestbook')
}两个关键步骤:
- 校验:服务端完成校验后,返回带
error字段的对象。客户端用它展示错误提示,页面不刷新,用户可以继续编辑。 - 缓存刷新与跳转:
revalidatePath('/guestbook')使该路由的缓存失效,下次请求重新渲染拿到最新数据;redirect('/guestbook')把浏览器重定向到该路径。这条调用链保证了用户提交后看到自己的留言出现在列表里。
useFormStatus 可以在子组件里读取父级 <form> 的 pending 状态:
tsx
'use client'
import { useFormStatus } from 'react-dom'
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? '提交中...' : '提交'}
</button>
)
}revalidatePath 按路由刷新,还有 revalidateTag 按缓存标签刷新。如果数据获取时通过 fetch(url, { next: { tags: ['messages'] } }) 打了标签,在 Server Action 里 revalidateTag('messages') 就能让所有关联标签的缓存同时失效。
综合示例:留言表单
把路由处理程序、中间件和 Server Actions 串起来。一个公开页面展示留言列表,一个表单提交新留言,服务端校验并写入,成功后刷新列表并跳转。
服务端:Server Actions 与 API 端点
app/guestbook/actions.ts 集中管理写操作:
ts
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
interface CreateMessageState {
error: string | null
}
export async function createMessage(
_prevState: CreateMessageState,
formData: FormData
): Promise<CreateMessageState> {
const content = String(formData.get('content') ?? '')
if (!content.trim()) {
return { error: '内容不能为空' }
}
// await db.message.create({ data: { content } })
revalidatePath('/guestbook')
redirect('/guestbook')
}如果需要 API 端点给外部消费,在 app/api/messages/route.ts 暴露 GET:
ts
import { NextResponse } from 'next/server'
export async function GET() {
// const messages = await db.message.findMany({ orderBy: { createdAt: 'desc' } })
const messages = [{ id: 1, content: 'hello' }]
return NextResponse.json(messages)
}中间件保护管理界面:
ts
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/admin/:path*', '/api/admin/:path*'],
}未登录用户访问 /admin/* 或 /api/admin/* 时直接跳转到 /login。
客户端:表单与错误展示
app/guestbook/page.tsx 作为服务端组件获取留言列表,嵌入客户端表单:
tsx
// app/guestbook/page.tsx
export default async function GuestbookPage() {
// const messages = await db.message.findMany({ orderBy: { createdAt: 'desc' } })
const messages = [{ id: 1, content: '你好' }, { id: 2, content: '世界' }]
return (
<main>
<h1>留言板</h1>
<MessageForm />
<ul>
{messages.map((msg) => (
<li key={msg.id}>{msg.content}</li>
))}
</ul>
</main>
)
}表单组件是客户端组件:
tsx
// app/guestbook/MessageForm.tsx
'use client'
import { useActionState } from 'react'
import { createMessage } from './actions'
import { useFormStatus } from 'react-dom'
function Submit() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? '提交中...' : '提交留言'}
</button>
)
}
export function MessageForm() {
const [state, formAction] = useActionState(createMessage, {
error: null,
})
return (
<form action={formAction}>
<textarea
name="content"
placeholder="写点什么..."
rows={3}
/>
{state?.error && <p style={{ color: 'red' }}>{state.error}</p>}
<Submit />
</form>
)
}数据流
用户点击“提交留言”:
formAction触发createMessageServer Action。- 服务端校验
content,不合法则返回{ error },表单显示错误——页面不刷新,用户可继续编辑。 - 校验通过,写入数据,
revalidatePath('/guestbook')让留言列表缓存失效,redirect('/guestbook')跳转回同一页面。 - 浏览器重定向后,服务端组件重新渲染,拉取最新留言列表,新留言出现在页面上。
整个过程没有客户端 fetch,没有显式调用 Route Handler,写操作全部封装在 Server Action 里。管理界面通过中间件做鉴权,未登录直接跳转。
参考链接
- Cloudflare 学习中心 — 什么是 API 端点? https://www.cloudflare.com/zh-cn/learning/security/api/what-is-api-endpoint
- Next.js 官方文档 — Route Handlers https://nextjs.org/docs/app/building-your-application/routing/route-handlers
- Next.js 官方文档 —
NextRequest/NextResponsehttps://nextjs.org/docs/app/api-reference/functions/next-request - Next.js 官方文档 — Middleware https://nextjs.org/docs/app/building-your-application/routing/middleware
- Next.js 官方文档 — Server Actions and Mutations https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations
- React 文档 —
useActionStatehttps://react.dev/reference/react/useActionState - React 文档 —
useFormStatushttps://react.dev/reference/react-dom/hooks/useFormStatus - Next.js 文档 —
revalidatePathhttps://nextjs.org/docs/app/api-reference/functions/revalidatePath
参考链接
- [1] https://www.cloudflare.com/zh-cn/learning/security/api/what-is-api-endpoint
- [2] https://nextjs.org/docs/app/building-your-application/routing/route-handlers
- [4] https://nextjs.org/docs/app/api-reference/functions/next-request
- [5] https://nextjs.org/docs/app/api-reference/functions/next-response
- [7] https://nextjs.org/docs/app/building-your-application/routing/middleware
- [10] https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations
- [12] https://react.dev/reference/react/useActionState
- [13] https://nextjs.org/docs/app/api-reference/functions/revalidatePath
相关推荐
2025/03/03 · 8 分钟Nuxt.js 核心组件与 API
Nuxt.js 核心组件与 API 的概念、用法、示例和注意点
2024/01/23 · 6 分钟Next.js 实战应用:构建全栈博客与常见问题Next.js 实战应用:构建全栈博客与常见问题 的概念、用法、示例和注意点
2024/01/21 · 10 分钟Next.js 进阶能力:流式渲染、并行路由与拦截路由Next.js 进阶能力:流式渲染、并行路由与拦截路由 的概念、用法、示例和注意点
2024/01/13 · 7 分钟Next.js 基本概念:文件系统路由与导航Next.js 基本概念:文件系统路由与导航 的概念、用法、示例和注意点
