切换语言
切换主题

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

上周五下午,产品经理走过来说:“能不能加个用户注册接口?”。我打开项目的 pages/api 文件夹,准备照着之前的套路写一个,结果发现这个文件夹是空的。后来才想起来,这是个用 App Router 的新项目,API 的写法完全变了。

打开 Next.js 文档,看到 “Route Handlers” 这个词,心里咯噔一下——又是新概念。花了一下午时间翻文档、看示例,才搞明白 route.ts 是个啥,为什么不能用之前熟悉的 reqres 了。

如果你也在为 Next.js 的后端接口写法感到困惑,这篇文章会帮你理清思路。我会用对比的方式讲清楚 Pages Router 和 App Router 的 API 写法到底变了什么,然后通过实战案例教你怎么处理请求、设计响应、优雅地处理错误。不用担心,看完这篇文章,你就能自信地用 Next.js 写后端接口了。

API Routes 基础:两种写法的本质区别

Pages Router 时代的写法

在 Next.js 13 之前,我们都是在 pages/api 文件夹里写接口。那时候的写法挺像 Express,用的是 Node.js 的 reqres 对象:

// 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 标准的 RequestResponse API:

// app/api/hello/route.ts
export async function GET(request: Request) {
  return Response.json({ message: 'Hello from Route Handlers!' })
}

第一次看到这个写法,我也觉得不习惯。为啥不能用之前的 reqres 了?后来才明白,这么改是有原因的:

  1. 拥抱 Web 标准:用浏览器原生的 RequestResponse API,代码更通用,也更符合现代 Web 开发的趋势
  2. 更好的类型安全:TypeScript 的支持更完善,不用额外装类型定义
  3. 支持 Edge Runtime:可以部署到 Vercel Edge、Cloudflare Workers 这些边缘环境,响应速度更快

核心区别对比

特性Pages RouterApp Router
文件位置pages/api/*app/*/route.ts
API 设计Node.js req/resWeb 标准 Request/Response
HTTP 方法单一默认导出,手动判断 req.method每个方法独立导出(GET、POST 等)
缓存行为不缓存GET 请求默认缓存

说实话,刚开始我也不理解为什么要改。用了一段时间之后才发现,新写法确实更清晰,尤其是处理不同 HTTP 方法的时候,不用再写一大堆 if (req.method === 'GET') 了。

Route Handlers 实战:创建和处理不同的 HTTP 请求

支持的 HTTP 方法

Route Handlers 支持 7 种 HTTP 方法:GETPOSTPUTPATCHDELETEHEADOPTIONS。每个方法对应一个命名导出的函数,这个设计我挺喜欢——代码结构一眼就能看清楚这个接口支持哪些操作。

下面是一个完整的用户管理接口示例:

// 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 提供了好几种方式来获取请求数据,刚开始我也搞混过,后来整理了一下:

  1. URL 参数:用 request.url 配合 URL 对象
const { searchParams } = new URL(request.url)
const keyword = searchParams.get('q')
  1. 请求体(JSON):用 await request.json()
const body = await request.json()
console.log(body.email) // 获取邮箱字段
  1. 请求体(FormData):用 await request.formData()
const formData = await request.formData()
const file = formData.get('avatar')
  1. 请求头和 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')

  // ...
}
  1. 动态路由参数:通过函数的第二个参数获取
// 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 之后,我发现错误可以分成两类:

  1. 预期错误:参数格式错误、资源不存在、权限不足——这些是正常的业务逻辑,应该返回 4xx 状态码
  2. 意外错误:数据库连接失败、第三方服务挂了、代码 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 更容易理解。

核心就是这几点:

  1. 用 HTTP 方法表达操作:

    • GET:获取资源
    • POST:创建资源
    • PUT/PATCH:更新资源
    • DELETE:删除资源
  2. 用 URL 表达资源:

    • /api/users - 用户列表
    • /api/users/123 - ID 为 123 的用户
    • /api/users/123/posts - 该用户的文章
  3. 用状态码表达结果:

    • 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。这个问题我遇到过好几次,原因通常是:

  1. 文件名写错了:必须是 route.tsroute.js,不能是 api.ts 或别的名字
  2. 位置不对:route.ts 不能和 page.tsx 在同一个文件夹
  3. 没提交到 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?

  1. 外部调用:给移动端 App、第三方服务提供接口
  2. Webhook:接收第三方服务的回调
  3. 客户端组件的数据变更:表单提交、删除操作等
  4. 复杂的业务逻辑:需要处理文件上传、调用多个外部 API

什么时候不需要 Route Handlers?

  1. Server Components 获取数据:直接查数据库更快
  2. 简单的表单提交:用 Server Actions 更简单
  3. 内部页面跳转:用 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 的优势是响应快,因为代码会部署到离用户最近的边缘节点。不过也有限制:

  1. 不能使用 Node.js API:比如 fspath 这些模块不能用
  2. 不能连接传统数据库:需要用支持 HTTP 连接的数据库,比如 Prisma Data Proxy、PlanetScale
  3. 包体积限制:代码不能太大,否则部署不上去

什么时候用 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 开发的思路——类型更安全、代码更清晰、部署更灵活。

接下来你可以:

  1. 马上动手:把现有项目的一个接口用 Route Handlers 重写,体会一下新写法的区别
  2. 建立模板:根据本文的错误处理和响应格式代码,整理一套自己的 API 模板,方便以后复用
  3. 持续学习:Next.js 还在快速发展,关注官方文档的更新,了解 Server Actions、Middleware 等新特性

记住,写 API 没有银弹,本文的方案也不是唯一正解。根据项目实际情况灵活调整,找到最适合自己团队的方案才是王道。

如果这篇文章对你有帮助,不妨收藏起来,遇到问题的时候翻出来看看。祝你用 Next.js 开发顺利!

常见问题

Route Handlers 和 Pages Router API Routes 的主要区别是什么?
主要区别有三点:1) Route Handlers 使用 Web 标准的 Request/Response API,而 Pages Router 使用 Node.js 的 req/res;2) 每个 HTTP 方法独立导出,不需要手动判断 req.method;3) GET 请求默认会被缓存,需手动配置 dynamic='force-dynamic' 禁用。
为什么我的 GET 请求返回的数据不是最新的?
Next.js App Router 的 GET 请求默认会被缓存。解决方法:在 route.ts 文件中添加 export const dynamic = 'force-dynamic',或在响应头中设置 Cache-Control: no-store。只有返回静态数据的接口才适合开启缓存。
什么时候应该用 Route Handlers,什么时候用 Server Components?
需要 Route Handlers 的场景:外部 API 调用、Webhook、客户端组件的数据变更、文件上传。不需要的场景:Server Components 获取数据可以直接查数据库,简单表单提交用 Server Actions 更方便。
如何优雅地处理 API 错误?
区分预期错误(4xx)和意外错误(5xx),使用统一的响应格式 { success, error, code, requestId }。针对不同操作分别 try-catch,避免一个大 try-catch 包所有逻辑。生产环境不返回敏感错误信息,只记录到日志。
部署后 API 全是 404,但本地正常,怎么办?
检查三点:1) 文件名必须是 route.ts 或 route.js;2) route.ts 不能和 page.tsx 在同一文件夹;3) 确认文件已提交到 git 且 next.config.js 没有排除 app 目录。常见错误是文件命名或位置不对。

13 分钟阅读 · 发布于: 2026年1月5日 · 修改于: 2026年1月22日

评论

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

相关文章