Next.js API Routes 完全指南:从 Route Handlers 到错误处理最佳实践

上周五下午,产品经理走过来说:“能不能加个用户注册接口?”。我打开项目的 pages/api 文件夹,准备照着之前的套路写一个,结果发现这个文件夹是空的。后来才想起来,这是个用 App Router 的新项目,API 的写法完全变了。
打开 Next.js 文档,看到 “Route Handlers” 这个词,心里咯噔一下——又是新概念。花了一下午时间翻文档、看示例,才搞明白 route.ts 是个啥,为什么不能用之前熟悉的 req 和 res 了。
如果你也在为 Next.js 的后端接口写法感到困惑,这篇文章会帮你理清思路。我会用对比的方式讲清楚 Pages Router 和 App Router 的 API 写法到底变了什么,然后通过实战案例教你怎么处理请求、设计响应、优雅地处理错误。不用担心,看完这篇文章,你就能自信地用 Next.js 写后端接口了。
API Routes 基础:两种写法的本质区别
Pages Router 时代的写法
在 Next.js 13 之前,我们都是在 pages/api 文件夹里写接口。那时候的写法挺像 Express,用的是 Node.js 的 req 和 res 对象:
// pages/api/hello.ts
import type { NextApiRequest, NextApiResponse } from 'next'
export default function handler(
req: NextApiRequest,
res: NextApiResponse
) {
res.status(200).json({ message: 'Hello from Pages Router!' })
}这个写法的好处是上手快,尤其是有 Node.js 或 Express 经验的人,基本不用学就会写。不过也有缺点——它依赖 Node.js 特定的 API,部署到边缘环境(Edge Runtime)会遇到问题。
App Router 时代的 Route Handlers
Next.js 13 引入 App Router 之后,API 的写法彻底变了。现在要在 app 目录下创建 route.ts 文件,用的是 Web 标准的 Request 和 Response API:
// app/api/hello/route.ts
export async function GET(request: Request) {
return Response.json({ message: 'Hello from Route Handlers!' })
}第一次看到这个写法,我也觉得不习惯。为啥不能用之前的 req 和 res 了?后来才明白,这么改是有原因的:
- 拥抱 Web 标准:用浏览器原生的
Request和ResponseAPI,代码更通用,也更符合现代 Web 开发的趋势 - 更好的类型安全:TypeScript 的支持更完善,不用额外装类型定义
- 支持 Edge Runtime:可以部署到 Vercel Edge、Cloudflare Workers 这些边缘环境,响应速度更快
核心区别对比
| 特性 | Pages Router | App Router |
|---|---|---|
| 文件位置 | pages/api/* | app/*/route.ts |
| API 设计 | Node.js req/res | Web 标准 Request/Response |
| HTTP 方法 | 单一默认导出,手动判断 req.method | 每个方法独立导出(GET、POST 等) |
| 缓存行为 | 不缓存 | GET 请求默认缓存 |
说实话,刚开始我也不理解为什么要改。用了一段时间之后才发现,新写法确实更清晰,尤其是处理不同 HTTP 方法的时候,不用再写一大堆 if (req.method === 'GET') 了。
Route Handlers 实战:创建和处理不同的 HTTP 请求
支持的 HTTP 方法
Route Handlers 支持 7 种 HTTP 方法:GET、POST、PUT、PATCH、DELETE、HEAD、OPTIONS。每个方法对应一个命名导出的函数,这个设计我挺喜欢——代码结构一眼就能看清楚这个接口支持哪些操作。
下面是一个完整的用户管理接口示例:
// app/api/users/route.ts
// 获取用户列表
export async function GET(request: Request) {
// 从 URL 获取查询参数
const { searchParams } = new URL(request.url)
const page = searchParams.get('page') || '1'
return Response.json({
users: [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
],
page: parseInt(page)
})
}
// 创建新用户
export async function POST(request: Request) {
// 解析 JSON 请求体
const body = await request.json()
return Response.json({
id: 3,
name: body.name
}, { status: 201 })
}处理请求数据
Next.js Route Handlers 提供了好几种方式来获取请求数据,刚开始我也搞混过,后来整理了一下:
- URL 参数:用
request.url配合URL对象
const { searchParams } = new URL(request.url)
const keyword = searchParams.get('q')- 请求体(JSON):用
await request.json()
const body = await request.json()
console.log(body.email) // 获取邮箱字段- 请求体(FormData):用
await request.formData()
const formData = await request.formData()
const file = formData.get('avatar')- 请求头和 Cookies:从
next/headers导入
import { headers, cookies } from 'next/headers'
export async function GET() {
const headersList = headers()
const cookieStore = cookies()
const token = headersList.get('authorization')
const userId = cookieStore.get('user_id')
// ...
}- 动态路由参数:通过函数的第二个参数获取
// app/api/users/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const userId = params.id
return Response.json({ userId })
}这里有个坑我踩过——params 在 Next.js 15+ 是异步的,需要用 await params.id,不过大多数情况下直接用就行,TypeScript 会提示你。
构造响应
返回 JSON 是最常见的场景,直接用 Response.json():
export async function GET() {
return Response.json({
success: true,
data: { message: '操作成功' }
})
}设置自定义状态码和响应头也很简单:
export async function POST(request: Request) {
const body = await request.json()
// 参数验证失败,返回 400
if (!body.email) {
return Response.json(
{ error: '邮箱不能为空' },
{ status: 400 }
)
}
// 创建成功,返回 201 并设置响应头
return Response.json(
{ id: 123, email: body.email },
{
status: 201,
headers: {
'X-Request-Id': 'abc-123',
'Cache-Control': 'no-cache'
}
}
)
}真实场景:用户注册接口
把上面的知识点串起来,写个完整的用户注册接口:
// app/api/auth/register/route.ts
import { headers } from 'next/headers'
export async function POST(request: Request) {
// 获取请求头
const headersList = headers()
const contentType = headersList.get('content-type')
// 检查 Content-Type
if (!contentType?.includes('application/json')) {
return Response.json(
{ error: '请使用 JSON 格式提交数据' },
{ status: 400 }
)
}
// 解析请求体
const body = await request.json()
const { username, email, password } = body
// 基础验证
if (!username || !email || !password) {
return Response.json(
{ error: '用户名、邮箱和密码不能为空' },
{ status: 400 }
)
}
// 这里应该调用数据库保存用户
// const user = await db.user.create({ username, email, password })
// 返回成功响应
return Response.json({
success: true,
data: {
id: 1,
username,
email
}
}, { status: 201 })
}这个例子覆盖了请求头检查、JSON 解析、参数验证、错误处理和成功响应,基本上大部分接口都是这个套路。
错误处理最佳实践:让 API 更稳定可靠
Try-Catch 的正确用法
刚开始写 API 的时候,我习惯在最外层包一个大 try-catch:
// ❌ 不推荐:一个大 try-catch 包所有逻辑
export async function POST(request: Request) {
try {
const body = await request.json()
// 一堆业务逻辑...
return Response.json({ success: true })
} catch (error) {
return Response.json({ error: '操作失败' }, { status: 500 })
}
}这样写有个问题——所有错误都返回 500,前端拿不到具体信息,调试起来很痛苦。后来我改成针对不同操作分别处理:
// ✅ 推荐:区分不同类型的错误
export async function POST(request: Request) {
let body
// 单独处理 JSON 解析错误
try {
body = await request.json()
} catch (error) {
return Response.json(
{ error: '请求格式错误,请检查 JSON 格式' },
{ status: 400 }
)
}
// 验证错误直接返回,不需要 try-catch
if (!body.email || !body.password) {
return Response.json(
{ error: '邮箱和密码不能为空' },
{ status: 400 }
)
}
// 单独处理数据库操作错误
try {
const user = await db.user.create(body)
return Response.json({ success: true, data: user })
} catch (error) {
// 检查是否是重复邮箱
if (error.code === 'P2002') {
return Response.json(
{ error: '该邮箱已被注册' },
{ status: 409 }
)
}
// 其他数据库错误
console.error('Database error:', error)
return Response.json(
{ error: '服务器错误,请稍后重试' },
{ status: 500 }
)
}
}这样改之后,错误信息清晰多了,前端也能根据状态码做不同处理。
结构化错误响应
我用过很多项目,发现错误响应格式五花八门,有时候是 { error: '...' },有时候是 { message: '...' },有时候是 { msg: '...' }。前端对接起来非常痛苦。
后来我总结了一套标准格式:
// 统一的错误响应格式
interface ErrorResponse {
success: false
error: string // 用户友好的错误信息
code?: string // 错误代码,方便前端做国际化
details?: any // 详细错误信息(开发环境使用)
requestId?: string // 请求追踪 ID
}
// 统一的成功响应格式
interface SuccessResponse<T> {
success: true
data: T
requestId?: string
}实际使用的时候,可以写个辅助函数:
// lib/api-response.ts
import { nanoid } from 'nanoid'
export function successResponse<T>(data: T, status: number = 200) {
return Response.json({
success: true,
data,
requestId: nanoid()
}, { status })
}
export function errorResponse(
error: string,
status: number = 500,
code?: string,
details?: any
) {
const isDev = process.env.NODE_ENV === 'development'
return Response.json({
success: false,
error,
code,
details: isDev ? details : undefined, // 生产环境不返回详细信息
requestId: nanoid()
}, { status })
}这样写接口就简洁多了:
// app/api/users/[id]/route.ts
import { successResponse, errorResponse } from '@/lib/api-response'
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const userId = params.id
try {
const user = await db.user.findUnique({ where: { id: userId } })
if (!user) {
return errorResponse('用户不存在', 404, 'USER_NOT_FOUND')
}
return successResponse(user)
} catch (error) {
return errorResponse(
'获取用户信息失败',
500,
'INTERNAL_ERROR',
error
)
}
}预期错误 vs 意外错误
写了一段时间 API 之后,我发现错误可以分成两类:
- 预期错误:参数格式错误、资源不存在、权限不足——这些是正常的业务逻辑,应该返回 4xx 状态码
- 意外错误:数据库连接失败、第三方服务挂了、代码 bug——这些是系统级错误,应该返回 5xx 状态码
对待这两类错误的方式也不一样:
export async function POST(request: Request) {
const body = await request.json()
// 预期错误:直接返回,不需要打日志
if (!body.email?.includes('@')) {
return errorResponse('邮箱格式不正确', 400, 'INVALID_EMAIL')
}
try {
// 调用外部 API
const response = await fetch('https://api.example.com/verify', {
method: 'POST',
body: JSON.stringify({ email: body.email })
})
if (!response.ok) {
// 第三方 API 返回错误,属于预期错误
return errorResponse('邮箱验证失败', 400, 'VERIFICATION_FAILED')
}
return successResponse({ verified: true })
} catch (error) {
// 网络错误、超时等,属于意外错误
console.error('Unexpected error:', error) // 记录日志
// 可以接入监控系统,如 Sentry
// Sentry.captureException(error)
return errorResponse(
'服务暂时不可用,请稍后重试',
503,
'SERVICE_UNAVAILABLE'
)
}
}避免敏感信息泄露
这个坑我在早期项目里踩过——直接把数据库错误信息返回给前端,结果把数据库表结构暴露了。正确的做法是:
try {
const user = await db.user.create(body)
return successResponse(user)
} catch (error) {
// ❌ 危险:直接返回原始错误
// return errorResponse(error.message, 500)
// ✅ 安全:返回通用错误,详细信息只在日志里
console.error('Database error:', {
error,
userId: request.headers.get('user-id'),
timestamp: new Date().toISOString()
})
return errorResponse(
'创建用户失败,请稍后重试',
500,
'CREATE_USER_FAILED'
)
}生产环境和开发环境也要区别对待:
const isDev = process.env.NODE_ENV === 'development'
return Response.json({
success: false,
error: '操作失败',
// 只在开发环境返回详细错误
stack: isDev ? error.stack : undefined,
details: isDev ? error : undefined
}, { status: 500 })响应格式设计:前后端协作的关键
RESTful API 设计原则
设计 API 的时候,我习惯遵循 RESTful 原则——不是说一定要严格遵守,但这套规则确实能让 API 更容易理解。
核心就是这几点:
用 HTTP 方法表达操作:
- GET:获取资源
- POST:创建资源
- PUT/PATCH:更新资源
- DELETE:删除资源
用 URL 表达资源:
/api/users- 用户列表/api/users/123- ID 为 123 的用户/api/users/123/posts- 该用户的文章
用状态码表达结果:
- 200:成功
- 201:创建成功
- 400:客户端参数错误
- 401:未登录
- 403:无权限
- 404:资源不存在
- 500:服务器错误
举个例子,用户管理 API 可以这样设计:
// app/api/users/route.ts
export async function GET(request: Request) {
// GET /api/users?page=1&limit=20
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '20')
const users = await db.user.findMany({
skip: (page - 1) * limit,
take: limit
})
return Response.json({ success: true, data: users })
}
export async function POST(request: Request) {
// POST /api/users
const body = await request.json()
const user = await db.user.create({ data: body })
return Response.json(
{ success: true, data: user },
{ status: 201 } // 注意这里用 201
)
}
// app/api/users/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
// GET /api/users/123
const user = await db.user.findUnique({ where: { id: params.id } })
if (!user) {
return Response.json(
{ success: false, error: '用户不存在' },
{ status: 404 }
)
}
return Response.json({ success: true, data: user })
}
export async function PATCH(
request: Request,
{ params }: { params: { id: string } }
) {
// PATCH /api/users/123
const body = await request.json()
const user = await db.user.update({
where: { id: params.id },
data: body
})
return Response.json({ success: true, data: user })
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
// DELETE /api/users/123
await db.user.delete({ where: { id: params.id } })
return Response.json({ success: true, data: null })
}统一的响应格式
前面讲错误处理的时候提到了统一响应格式,这里再完善一下。我现在用的格式是这样的:
// 基础响应类型
type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: string; code?: string }
// 分页响应
interface PaginatedResponse<T> {
success: true
data: T[]
pagination: {
page: number
limit: number
total: number
totalPages: number
}
}
// 列表响应示例
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '20')
const [users, total] = await Promise.all([
db.user.findMany({ skip: (page - 1) * limit, take: limit }),
db.user.count()
])
return Response.json({
success: true,
data: users,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
})
}TypeScript 类型定义
既然前后端都用 TypeScript,那就让类型定义也共享起来。我一般会在项目里创建一个 types 文件夹:
// types/api.ts
export interface User {
id: string
username: string
email: string
createdAt: string
}
export interface CreateUserRequest {
username: string
email: string
password: string
}
export interface CreateUserResponse {
success: true
data: User
}
// types/api-client.ts
import type { CreateUserRequest, CreateUserResponse } from './api'
export async function createUser(data: CreateUserRequest) {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
const result: CreateUserResponse = await response.json()
if (!result.success) {
throw new Error(result.error)
}
return result.data
}这样前端调用接口的时候就有完整的类型提示了,写代码舒服多了。
常见问题和解决方案
GET 请求默认缓存的问题
这个坑我踩得最惨。有次写了个查询用户信息的接口,本地测试一切正常,部署之后发现修改用户信息后,前端拿到的还是旧数据。排查了半天才发现,Next.js 的 GET 请求默认会被缓存。
为什么会这样?Next.js 团队的想法是——很多 GET 请求返回的是静态数据,缓存可以提升性能。但实际开发中,大部分 GET 请求都是动态的,需要实时数据。
解决方法很简单,在 route.ts 文件里加一行配置:
// app/api/users/route.ts
export const dynamic = 'force-dynamic' // 禁用缓存
export async function GET() {
const users = await db.user.findMany()
return Response.json({ success: true, data: users })
}还有其他几种方法:
// 方法2:设置 revalidate 为 0
export const revalidate = 0
// 方法3:在响应头里设置 Cache-Control
export async function GET() {
const users = await db.user.findMany()
return Response.json(
{ success: true, data: users },
{
headers: {
'Cache-Control': 'no-store, max-age=0'
}
}
)
}什么时候需要缓存?如果你的接口返回的是基本不变的数据,比如国家列表、分类列表,那可以利用缓存:
// app/api/countries/route.ts
export const revalidate = 3600 // 缓存 1 小时
export async function GET() {
const countries = await db.country.findMany()
return Response.json({ success: true, data: countries })
}部署后 API 404 的问题
本地开发一切正常,部署到 Vercel 或 Netlify 之后,接口全都 404。这个问题我遇到过好几次,原因通常是:
- 文件名写错了:必须是
route.ts或route.js,不能是api.ts或别的名字 - 位置不对:
route.ts不能和page.tsx在同一个文件夹 - 没提交到 git:检查一下
.gitignore,确保 route 文件被提交了
检查清单:
# ✅ 正确的结构
app/
api/
users/
route.ts # 正确:GET /api/users
users/
[id]/
route.ts # 正确:GET /api/users/123
# ❌ 错误的结构
app/
api/
users.ts # 错误:应该是 route.ts
users/
page.tsx
route.ts # 错误:不能和 page.tsx 同级还有一个容易忽略的点——确保你的 next.config.js 里没有排除 app 目录:
// next.config.js
module.exports = {
// 不要有这行配置,否则 app 目录会被忽略
// pageExtensions: ['page.tsx', 'page.ts'],
}Redirect 在 try-catch 中的问题
Next.js 的 redirect() 函数会抛出一个特殊错误来实现重定向,如果你把它放在 try-catch 里,重定向就不会生效:
import { redirect } from 'next/navigation'
// ❌ 错误写法
export async function GET() {
try {
const isLoggedIn = await checkAuth()
if (!isLoggedIn) {
redirect('/login') // 这个重定向会被 catch 捕获
}
return Response.json({ success: true })
} catch (error) {
return Response.json({ error: '操作失败' }, { status: 500 })
}
}
// ✅ 正确写法
export async function GET() {
const isLoggedIn = await checkAuth()
if (!isLoggedIn) {
redirect('/login') // 在 try-catch 外调用
}
try {
const data = await fetchData()
return Response.json({ success: true, data })
} catch (error) {
return Response.json({ error: '操作失败' }, { status: 500 })
}
}不过说实话,在 Route Handlers 里用 redirect() 的场景不多,大部分时候直接返回 401 状态码,让前端去处理跳转就行了。
什么时候不需要 Route Handlers
刚接触 App Router 的时候,我以为所有数据获取都要写 API 接口。后来才发现,Server Components 可以直接调用后端逻辑,根本不需要绕一圈走 HTTP 请求。
比如这个场景:
// ❌ 不推荐:先写 API,再从组件调用
// app/api/posts/route.ts
export async function GET() {
const posts = await db.post.findMany()
return Response.json({ success: true, data: posts })
}
// app/blog/page.tsx
async function BlogPage() {
const res = await fetch('http://localhost:3000/api/posts')
const { data } = await res.json()
return <div>{/* 渲染文章列表 */}</div>
}
// ✅ 推荐:Server Component 直接查询
// app/blog/page.tsx
async function BlogPage() {
const posts = await db.post.findMany() // 直接查数据库
return <div>{/* 渲染文章列表 */}</div>
}什么时候需要 Route Handlers?
- 外部调用:给移动端 App、第三方服务提供接口
- Webhook:接收第三方服务的回调
- 客户端组件的数据变更:表单提交、删除操作等
- 复杂的业务逻辑:需要处理文件上传、调用多个外部 API
什么时候不需要 Route Handlers?
- Server Components 获取数据:直接查数据库更快
- 简单的表单提交:用 Server Actions 更简单
- 内部页面跳转:用 Next.js 的路由就行
进阶技巧:让 API 更专业
输入验证
前面的例子里,参数验证都是手写 if 判断,这样写太麻烦了。我现在用 Zod 来做验证:
import { z } from 'zod'
// 定义验证规则
const createUserSchema = z.object({
username: z.string().min(3).max(20),
email: z.string().email(),
password: z.string().min(8),
age: z.number().min(18).optional()
})
export async function POST(request: Request) {
const body = await request.json()
// 验证数据
const result = createUserSchema.safeParse(body)
if (!result.success) {
return Response.json({
success: false,
error: '数据验证失败',
details: result.error.errors // 返回详细的验证错误
}, { status: 400 })
}
// result.data 是验证后的数据,类型安全
const user = await db.user.create({ data: result.data })
return Response.json({ success: true, data: user }, { status: 201 })
}Zod 的好处是验证和类型定义合二为一:
// 从 schema 推导出 TypeScript 类型
type CreateUserInput = z.infer<typeof createUserSchema>
// 等价于:
// type CreateUserInput = {
// username: string
// email: string
// password: string
// age?: number
// }中间件模式
写了几个接口之后,会发现很多重复代码——身份验证、日志记录、错误处理。这时候可以抽象成中间件:
// lib/middleware.ts
type RouteHandler = (request: Request, context: any) => Promise<Response>
// 认证中间件
export function withAuth(handler: RouteHandler): RouteHandler {
return async (request, context) => {
const token = request.headers.get('authorization')
if (!token) {
return Response.json(
{ success: false, error: '未登录' },
{ status: 401 }
)
}
// 验证 token
const user = await verifyToken(token)
if (!user) {
return Response.json(
{ success: false, error: 'Token 无效' },
{ status: 401 }
)
}
// 把用户信息传给 handler
context.user = user
return handler(request, context)
}
}
// 日志中间件
export function withLogging(handler: RouteHandler): RouteHandler {
return async (request, context) => {
const start = Date.now()
const { method, url } = request
console.log(`[${method}] ${url} - 开始处理`)
const response = await handler(request, context)
const duration = Date.now() - start
console.log(`[${method}] ${url} - 完成 (${duration}ms)`)
return response
}
}
// 组合使用
// app/api/profile/route.ts
import { withAuth, withLogging } from '@/lib/middleware'
async function getProfile(request: Request, context: any) {
const user = context.user // 从中间件获取用户信息
return Response.json({ success: true, data: user })
}
export const GET = withLogging(withAuth(getProfile))这个模式挺好用,我在实际项目里会结合 Zod 验证一起用:
// lib/middleware.ts
export function withValidation<T>(
schema: z.Schema<T>,
handler: (request: Request, data: T, context: any) => Promise<Response>
): RouteHandler {
return async (request, context) => {
const body = await request.json()
const result = schema.safeParse(body)
if (!result.success) {
return Response.json({
success: false,
error: '数据验证失败',
details: result.error.errors
}, { status: 400 })
}
return handler(request, result.data, context)
}
}
// 使用
export const POST = withAuth(
withValidation(createUserSchema, async (request, data, context) => {
// data 已经过验证,类型安全
const user = await db.user.create({ data })
return Response.json({ success: true, data: user }, { status: 201 })
})
)Edge Runtime 的选择
Next.js 支持两种运行时:Node.js Runtime 和 Edge Runtime。大部分情况下默认的 Node.js Runtime 就够了,但如果你的 API 需要全球快速响应,可以考虑 Edge Runtime:
// app/api/hello/route.ts
export const runtime = 'edge' // 指定使用 Edge Runtime
export async function GET() {
return Response.json({ message: 'Hello from Edge!' })
}Edge Runtime 的优势是响应快,因为代码会部署到离用户最近的边缘节点。不过也有限制:
- 不能使用 Node.js API:比如
fs、path这些模块不能用 - 不能连接传统数据库:需要用支持 HTTP 连接的数据库,比如 Prisma Data Proxy、PlanetScale
- 包体积限制:代码不能太大,否则部署不上去
什么时候用 Edge Runtime?
- 简单的 API,不需要复杂依赖
- 读多写少的场景
- 需要全球低延迟
什么时候用 Node.js Runtime?
- 需要连接传统数据库
- 需要用 Node.js 生态的库
- 复杂的业务逻辑
说实话,我大部分项目都用默认的 Node.js Runtime,Edge Runtime 目前还是适合特定场景。
结论
写到这里,Next.js API Routes 的核心内容基本讲完了。回顾一下:
- 写法变化:从 Pages Router 的
req/res到 App Router 的Request/Response,拥抱 Web 标准 - Route Handlers:每个 HTTP 方法独立导出,结构清晰,支持 GET、POST、PUT、PATCH、DELETE 等
- 请求处理:URL 参数、JSON body、FormData、Headers、Cookies、动态路由参数——各种数据获取方式
- 错误处理:区分预期错误和意外错误,统一响应格式,避免敏感信息泄露
- 响应设计:遵循 RESTful 原则,状态码语义化,TypeScript 类型共享
- 常见坑点:GET 缓存、部署 404、redirect 在 try-catch 中失效、过度使用 Route Handlers
老实说,Next.js 的 API 写法从 Pages Router 到 App Router 确实变化挺大,刚开始会有点不适应。但用一段时间之后,你会发现新写法更符合现代 Web 开发的思路——类型更安全、代码更清晰、部署更灵活。
接下来你可以:
- 马上动手:把现有项目的一个接口用 Route Handlers 重写,体会一下新写法的区别
- 建立模板:根据本文的错误处理和响应格式代码,整理一套自己的 API 模板,方便以后复用
- 持续学习:Next.js 还在快速发展,关注官方文档的更新,了解 Server Actions、Middleware 等新特性
记住,写 API 没有银弹,本文的方案也不是唯一正解。根据项目实际情况灵活调整,找到最适合自己团队的方案才是王道。
如果这篇文章对你有帮助,不妨收藏起来,遇到问题的时候翻出来看看。祝你用 Next.js 开发顺利!
常见问题
Route Handlers 和 Pages Router API Routes 的主要区别是什么?
为什么我的 GET 请求返回的数据不是最新的?
什么时候应该用 Route Handlers,什么时候用 Server Components?
如何优雅地处理 API 错误?
部署后 API 全是 404,但本地正常,怎么办?
13 分钟阅读 · 发布于: 2026年1月5日 · 修改于: 2026年1月22日
相关文章
Next.js 电商实战:购物车与 Stripe 支付完整实现指南

Next.js 电商实战:购物车与 Stripe 支付完整实现指南
Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战

Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战
Next.js 单元测试实战:Jest + React Testing Library 完整配置指南


评论
使用 GitHub 账号登录后即可评论