Next.js

路由处理器

深入理解 Next.js 路由处理器,包括 HTTP 方法、请求响应处理、流式传输、Cookie、Headers 和动态路由

概述

Route Handlers 允许你使用 Web Request 和 Response API 为给定路由创建自定义请求处理器。

Route Handlers 仅在 app 目录中可用。它们相当于 pages 目录中的 API Routes,这意味着你不需要同时使用 API Routes 和 Route Handlers。

约定

Route Handlers 在 app 目录内的 route.js|ts 文件中定义:

export async function GET(request: Request) {
  return Response.json({ message: 'Hello World' })
}

Route Handlers 可以嵌套在 app 目录内的任何位置,类似于 page.jslayout.js。但在与 page.js 相同的路由段级别不能有 route.js 文件。

支持的 HTTP 方法

支持以下 HTTP 方法:GETPOSTPUTPATCHDELETEHEADOPTIONS

export async function GET(request: Request) {
  return Response.json({ message: 'GET request' })
}

export async function POST(request: Request) {
  return Response.json({ message: 'POST request' })
}

export async function PUT(request: Request) {
  return Response.json({ message: 'PUT request' })
}

export async function PATCH(request: Request) {
  return Response.json({ message: 'PATCH request' })
}

export async function DELETE(request: Request) {
  return Response.json({ message: 'DELETE request' })
}

export async function HEAD(request: Request) {
  return new Response(null)
}

export async function OPTIONS(request: Request) {
  return new Response(null, {
    headers: {
      Allow: 'GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS',
    },
  })
}

如果调用不支持的方法,Next.js 将返回 405 Method Not Allowed 响应。

NextRequest 和 NextResponse

Next.js 扩展了原生 Request 和 Response API,提供了 NextRequestNextResponse,为高级用例提供便捷的辅助函数。

import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams
  const query = searchParams.get('query')

  return NextResponse.json({ query })
}

请求处理

读取请求体

export async function POST(request: Request) {
  const body = await request.json()
  
  return Response.json({
    message: 'User created',
    user: body,
  })
}

读取查询参数

import { NextRequest } from 'next/server'

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams
  const query = searchParams.get('query')
  const page = searchParams.get('page') || '1'

  return Response.json({ query, page })
}

读取 Headers

export async function GET(request: Request) {
  const headersList = request.headers
  const userAgent = headersList.get('user-agent')
  const authorization = headersList.get('authorization')

  return Response.json({ userAgent, authorization })
}

读取 Cookies

import { cookies } from 'next/headers'

export async function GET(request: Request) {
  const cookieStore = await cookies()
  const token = cookieStore.get('token')

  return Response.json({ token: token?.value })
}

响应处理

返回 JSON

export async function GET() {
  return Response.json({ message: 'Hello World' })
}

返回文本

export async function GET() {
  return new Response('Hello World', {
    headers: {
      'Content-Type': 'text/plain',
    },
  })
}

返回 HTML

export async function GET() {
  return new Response('<h1>Hello World</h1>', {
    headers: {
      'Content-Type': 'text/html',
    },
  })
}

设置状态码

export async function POST(request: Request) {
  const body = await request.json()

  return Response.json(
    { message: 'Created successfully' },
    { status: 201 }
  )
}

设置 Headers

export async function GET() {
  return new Response('Hello World', {
    headers: {
      'Content-Type': 'text/plain',
      'X-Custom-Header': 'custom-value',
    },
  })
}

设置 Cookies

import { NextResponse } from 'next/server'

export async function GET() {
  const response = NextResponse.json({ message: 'Hello World' })

  response.cookies.set('token', 'abc123', {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 60 * 60 * 24 * 7, // 1 week
  })

  return response
}

删除 Cookies

import { NextResponse } from 'next/server'

export async function GET() {
  const response = NextResponse.json({ message: 'Logged out' })

  response.cookies.delete('token')

  return response
}

重定向

import { redirect } from 'next/navigation'

export async function GET() {
  redirect('https://nextjs.org/')
}

使用 NextResponse:

import { NextResponse } from 'next/server'

export async function GET() {
  return NextResponse.redirect(new URL('/new-path', request.url))
}

动态路由

路由参数

export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params
  
  return Response.json({ postId: id })
}

多个路由参数

export async function GET(
  request: Request,
  { params }: { params: Promise<{ category: string; id: string }> }
) {
  const { category, id } = await params
  
  return Response.json({ category, postId: id })
}

Catch-all 路由

export async function GET(
  request: Request,
  { params }: { params: Promise<{ path: string[] }> }
) {
  const { path } = await params
  
  return Response.json({ path })
}

流式传输

流式响应

export async function GET() {
  const encoder = new TextEncoder()

  const customReadable = new ReadableStream({
    async start(controller) {
      controller.enqueue(encoder.encode('Hello '))
      await new Promise((resolve) => setTimeout(resolve, 1000))
      controller.enqueue(encoder.encode('World'))
      controller.close()
    },
  })

  return new Response(customReadable, {
    headers: {
      'Content-Type': 'text/plain',
      'Transfer-Encoding': 'chunked',
    },
  })
}

使用 AI SDK 流式传输

import { openai } from '@ai-sdk/openai'
import { streamText } from 'ai'

export async function POST(request: Request) {
  const { messages } = await request.json()

  const result = streamText({
    model: openai('gpt-4-turbo'),
    messages,
  })

  return result.toDataStreamResponse()
}

CORS

设置 CORS Headers

export async function GET(request: Request) {
  return new Response('Hello World', {
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    },
  })
}

处理 OPTIONS 请求

export async function OPTIONS(request: Request) {
  return new Response(null, {
    status: 204,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    },
  })
}

非 UI 响应

返回 sitemap.xml

export async function GET() {
  return new Response(
    `<?xml version="1.0" encoding="UTF-8" ?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://example.com</loc>
    <lastmod>2023-04-06</lastmod>
  </url>
</urlset>`,
    {
      headers: {
        'Content-Type': 'text/xml',
      },
    }
  )
}

返回 robots.txt

export async function GET() {
  return new Response(
    `User-agent: *
Allow: /
Disallow: /private/

Sitemap: https://example.com/sitemap.xml`,
    {
      headers: {
        'Content-Type': 'text/plain',
      },
    }
  )
}

返回 RSS Feed

export async function GET() {
  return new Response(
    `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
  <channel>
    <title>My Blog</title>
    <link>https://example.com</link>
    <description>My blog description</description>
  </channel>
</rss>`,
    {
      headers: {
        'Content-Type': 'text/xml',
      },
    }
  )
}

段配置选项

Route Handlers 使用与 pages 和 layouts 相同的路由段配置。

export const dynamic = 'auto'
export const dynamicParams = true
export const revalidate = false
export const fetchCache = 'auto'
export const runtime = 'nodejs'
export const preferredRegion = 'auto'
export const maxDuration = 5

export async function GET() {
  return Response.json({ message: 'Hello World' })
}

dynamic

控制路由的动态行为:

export const dynamic = 'force-dynamic' // 'auto' | 'force-dynamic' | 'error' | 'force-static'

export async function GET() {
  return Response.json({ timestamp: Date.now() })
}

revalidate

设置路由的重新验证时间(秒):

export const revalidate = 60 // 每 60 秒重新验证

export async function GET() {
  const data = await fetch('https://api.example.com/data')
  return Response.json(data)
}

runtime

指定运行时环境:

export const runtime = 'edge' // 'nodejs' | 'edge'

export async function GET() {
  return Response.json({ message: 'Running on Edge' })
}

错误处理

基本错误处理

export async function POST(request: Request) {
  try {
    const body = await request.json()
    
    if (!body.email) {
      return Response.json(
        { error: 'Email is required' },
        { status: 400 }
      )
    }

    // 创建用户逻辑
    
    return Response.json({ message: 'User created' }, { status: 201 })
  } catch (error) {
    return Response.json(
      { error: 'Internal server error' },
      { status: 500 }
    )
  }
}

自定义错误响应

class APIError extends Error {
  constructor(public statusCode: number, message: string) {
    super(message)
  }
}

export async function GET() {
  try {
    throw new APIError(404, 'Resource not found')
  } catch (error) {
    if (error instanceof APIError) {
      return Response.json(
        { error: error.message },
        { status: error.statusCode }
      )
    }

    return Response.json(
      { error: 'Internal server error' },
      { status: 500 }
    )
  }
}

身份验证

验证 Token

export async function GET(request: Request) {
  const token = request.headers.get('authorization')?.split(' ')[1]

  if (!token) {
    return Response.json(
      { error: 'Unauthorized' },
      { status: 401 }
    )
  }

  try {
    // 验证 token
    const user = await verifyToken(token)
    
    return Response.json({ user })
  } catch (error) {
    return Response.json(
      { error: 'Invalid token' },
      { status: 401 }
    )
  }
}

使用 Cookies 进行身份验证

import { cookies } from 'next/headers'

export async function GET() {
  const cookieStore = await cookies()
  const sessionToken = cookieStore.get('session')

  if (!sessionToken) {
    return Response.json(
      { error: 'Unauthorized' },
      { status: 401 }
    )
  }

  const user = await getUserFromSession(sessionToken.value)

  return Response.json({ user })
}

文件上传

处理文件上传

import { writeFile } from 'fs/promises'
import { NextRequest } from 'next/server'
import path from 'path'

export async function POST(request: NextRequest) {
  const formData = await request.formData()
  const file = formData.get('file') as File

  if (!file) {
    return Response.json(
      { error: 'No file uploaded' },
      { status: 400 }
    )
  }

  const bytes = await file.arrayBuffer()
  const buffer = Buffer.from(bytes)

  const filePath = path.join(process.cwd(), 'public', 'uploads', file.name)
  await writeFile(filePath, buffer)

  return Response.json({ 
    message: 'File uploaded successfully',
    filename: file.name 
  })
}

代理请求

转发请求到外部 API

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const endpoint = searchParams.get('endpoint')

  if (!endpoint) {
    return Response.json(
      { error: 'Endpoint is required' },
      { status: 400 }
    )
  }

  const response = await fetch(`https://api.example.com/${endpoint}`, {
    headers: {
      'Authorization': `Bearer ${process.env.API_KEY}`,
    },
  })

  const data = await response.json()

  return Response.json(data)
}

最佳实践

1. 使用 TypeScript 类型

import { NextRequest, NextResponse } from 'next/server'

type User = {
  id: string
  name: string
  email: string
}

export async function GET(request: NextRequest): Promise<NextResponse<User[]>> {
  const users: User[] = await getUsers()
  return NextResponse.json(users)
}

2. 验证输入数据

import { z } from 'zod'

const userSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
})

export async function POST(request: Request) {
  try {
    const body = await request.json()
    const validatedData = userSchema.parse(body)

    // 使用验证后的数据
    return Response.json({ user: validatedData }, { status: 201 })
  } catch (error) {
    if (error instanceof z.ZodError) {
      return Response.json(
        { error: error.errors },
        { status: 400 }
      )
    }

    return Response.json(
      { error: 'Internal server error' },
      { status: 500 }
    )
  }
}

3. 使用环境变量

export async function GET() {
  const apiKey = process.env.API_KEY

  if (!apiKey) {
    return Response.json(
      { error: 'API key not configured' },
      { status: 500 }
    )
  }

  const response = await fetch('https://api.example.com/data', {
    headers: {
      'Authorization': `Bearer ${apiKey}`,
    },
  })

  const data = await response.json()
  return Response.json(data)
}

4. 实现速率限制

const rateLimit = new Map<string, number[]>()

function checkRateLimit(ip: string): boolean {
  const now = Date.now()
  const windowMs = 60 * 1000 // 1 minute
  const maxRequests = 10

  const requests = rateLimit.get(ip) || []
  const recentRequests = requests.filter((time) => now - time < windowMs)

  if (recentRequests.length >= maxRequests) {
    return false
  }

  recentRequests.push(now)
  rateLimit.set(ip, recentRequests)
  return true
}

export async function GET(request: Request) {
  const ip = request.headers.get('x-forwarded-for') || 'unknown'

  if (!checkRateLimit(ip)) {
    return Response.json(
      { error: 'Too many requests' },
      { status: 429 }
    )
  }

  return Response.json({ message: 'Success' })
}

5. 使用缓存

export const revalidate = 3600 // 缓存 1 小时

export async function GET() {
  const data = await fetch('https://api.example.com/data')
  return Response.json(data)
}

6. 处理 CORS

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}

export async function OPTIONS() {
  return new Response(null, {
    status: 204,
    headers: corsHeaders,
  })
}

export async function GET() {
  return Response.json(
    { message: 'Hello World' },
    { headers: corsHeaders }
  )
}

总结

Next.js Route Handlers 提供了强大的 API 路由功能:

  1. HTTP 方法:支持所有标准 HTTP 方法
  2. 请求处理:读取请求体、查询参数、Headers 和 Cookies
  3. 响应处理:返回 JSON、文本、HTML,设置状态码和 Headers
  4. 动态路由:支持路由参数和 catch-all 路由
  5. 流式传输:支持流式响应
  6. CORS:轻松配置跨域资源共享
  7. 段配置:控制缓存、运行时和重新验证行为
  8. 错误处理:完善的错误处理机制
  9. 身份验证:支持 Token 和 Cookie 身份验证
  10. 文件上传:处理文件上传

通过合理使用 Route Handlers,可以构建强大、安全、高性能的 API 端点。

在 GitHub 上编辑

上次更新于