Chapter 08

API Routes 与中间件

Route Handlers、Edge Runtime 与 middleware.ts 构建高性能 API 层

8.1 Route Handlers 概述

在 App Router 中,API 端点通过 route.ts(或 route.js)文件定义,称为 Route Handlers。与 Pages Router 的 pages/api/ 不同,Route Handlers 使用标准的 Web API(RequestResponse),不依赖 Node.js 特定的 req/res,因此可以运行在 Edge Runtime。

Route Handlers 支持所有 HTTP 方法:GETPOSTPUTPATCHDELETEHEADOPTIONS。每个方法对应文件中导出的同名函数。

ℹ️

Route Handler vs Server Action对于需要从前端触发的数据变更操作,优先使用 Server Actions(更简单,内置 CSRF 保护)。Route Handlers 更适合:需要对外提供的 REST API、Webhook 回调、需要自定义 Response headers 的场景、以及与第三方服务集成。

8.2 创建 Route Handler

// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
import { auth } from '@/auth'

// GET /api/posts — 获取文章列表
export async function GET(request: NextRequest) {
  // 读取 URL 查询参数
  const searchParams = request.nextUrl.searchParams
  const page  = Number(searchParams.get('page')  ?? 1)
  const limit = Number(searchParams.get('limit') ?? 10)

  const posts = await db.post.findMany({
    where:   { published: true },
    skip:    (page - 1) * limit,
    take:    limit,
    orderBy: { createdAt: 'desc' },
    select:  { id: true, title: true, slug: true }
  })

  // 返回 JSON 响应
  return NextResponse.json({ posts, page, limit })
}

// POST /api/posts — 创建文章(需要认证)
export async function POST(request: NextRequest) {
  const session = await auth()

  if (!session?.user) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    )
  }

  const body = await request.json()
  const { title, content } = body

  if (!title) {
    return NextResponse.json(
      { error: 'Title is required' },
      { status: 400 }
    )
  }

  const post = await db.post.create({
    data: { title, content, authorId: session.user.id }
  })

  return NextResponse.json(post, { status: 201 })
}
TS

8.3 动态 Route Handlers

与页面路由相同,Route Handlers 也支持动态参数 [id]。参数通过第二个参数的 params 传入。

// app/api/posts/[id]/route.ts
interface RouteContext {
  params: Promise<{ id: string }>
}

// GET /api/posts/:id
export async function GET(
  request: NextRequest,
  { params }: RouteContext
) {
  const { id } = await params
  const post = await db.post.findUnique({ where: { id } })

  if (!post) {
    return NextResponse.json({ error: 'Not found' }, { status: 404 })
  }

  return NextResponse.json(post)
}

// PATCH /api/posts/:id
export async function PATCH(
  request: NextRequest,
  { params }: RouteContext
) {
  const session = await auth()
  if (!session?.user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const { id } = await params
  const body = await request.json()

  const updated = await db.post.update({
    where: { id, authorId: session.user.id }, // 确保只能修改自己的文章
    data:  body,
  })

  return NextResponse.json(updated)
}

// DELETE /api/posts/:id
export async function DELETE(
  request: NextRequest,
  { params }: RouteContext
) {
  const session = await auth()
  if (!session?.user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const { id } = await params
  await db.post.delete({ where: { id, authorId: session.user.id } })

  return new Response(null, { status: 204 })
}
TS

8.4 Webhook 处理

Webhook 是第三方服务(Stripe、GitHub、Clerk 等)主动推送事件到你的 API 的机制。处理 Webhook 时需要注意:验证签名、使用原始 Body(不能解析 JSON)、快速响应(200/204),耗时操作异步处理。

// app/api/webhooks/stripe/route.ts
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST(request: Request) {
  // 获取原始 Body(Stripe 签名验证需要)
  const body      = await request.text()
  const signature = request.headers.get('stripe-signature')!

  let event: Stripe.Event

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    )
  } catch (err) {
    return new Response('Invalid signature', { status: 400 })
  }

  // 处理事件(异步,不阻塞响应)
  switch (event.type) {
    case 'checkout.session.completed':
      await handleCheckout(event.data.object)
      break
    case 'customer.subscription.deleted':
      await handleCancellation(event.data.object)
      break
  }

  return new Response('OK', { status: 200 })
}
TS

8.5 middleware.ts 深度解析

middleware.ts 在 Edge Runtime 上运行,在请求到达任何路由(页面或 API)之前执行。它的执行时机决定了其用途:认证检查、A/B 测试、国际化重定向、请求日志、速率限制等。

由于运行在 Edge Runtime,middleware 不能使用 Node.js 专有 API(如 fspath),也不能直接访问数据库。如果需要数据库操作,应在页面/API 中进行,middleware 只做轻量级的权限判断。

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
  const { pathname, searchParams } = request.nextUrl

  // 1. 国际化:根据 Accept-Language 重定向
  const locale = request.headers.get('accept-language')?.[0] ?? 'en'
  if (pathname === '/') {
    return NextResponse.redirect(new URL(`/${locale}`, request.url))
  }

  // 2. 克隆请求并添加自定义 Header
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-pathname', pathname)

  const response = NextResponse.next({
    request: { headers: requestHeaders },
  })

  // 3. 在响应中设置 Cookie
  response.cookies.set('visited', 'true', {
    httpOnly: true,
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 7, // 7天
  })

  return response
}

// 精确控制 middleware 匹配的路径
export const config = {
  matcher: [
    // 匹配所有路径,排除 Next.js 内部和静态文件
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
}
TS

8.6 Edge Runtime vs Node.js Runtime

Next.js 支持两种运行时,可以在路由段级别声明:

特性 Node.js Runtime(默认) Edge Runtime
冷启动时间~300ms<1ms
Node.js API完整支持不支持
数据库访问直接连接需要 HTTP API 或连接池
Bundle 大小限制无限制4MB
适合场景复杂业务逻辑、数据库操作认证、重定向、A/B测试
部署位置服务器全球边缘节点
// 在路由段中声明使用 Edge Runtime
export const runtime = 'edge'

export async function GET(request: Request) {
  // 这个 Route Handler 将在 Edge 上运行
  return new Response('Hello from Edge!')
}
TS
💡

CORS 配置如果 API 需要跨域访问,在 Route Handler 的响应中添加 CORS 头:Access-Control-Allow-Origin: *(或指定域名)。也可以在 next.config.tsheaders() 函数中全局配置,避免在每个路由重复设置。