Skip to content

Route Handlers

🙋 需要创建 REST API 或处理 Webhook?Route Handlers 让你在 App Router 中轻松构建 API 端点。

基础用法#

创建 Route Handler#

app/
└── api/
└── hello/
└── route.ts # /api/hello
app/api/hello/route.ts
// Next.js 15.x
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({ message: 'Hello, World!' })
}

HTTP 方法#

app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'
// GET /api/posts
export async function GET() {
const posts = await db.post.findMany()
return NextResponse.json(posts)
}
// POST /api/posts
export async function POST(request: NextRequest) {
const data = await request.json()
const post = await db.post.create({ data })
return NextResponse.json(post, { status: 201 })
}
app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
// GET /api/posts/:id
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
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)
}
// PUT /api/posts/:id
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const data = await request.json()
const post = await db.post.update({
where: { id },
data,
})
return NextResponse.json(post)
}
// DELETE /api/posts/:id
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
await db.post.delete({ where: { id } })
return new NextResponse(null, { status: 204 })
}

支持的 HTTP 方法:GETPOSTPUTPATCHDELETEHEADOPTIONS

请求处理#

获取请求数据#

app/api/submit/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
// JSON 数据
const json = await request.json()
// FormData
const formData = await request.formData()
const name = formData.get('name')
// 文本
const text = await request.text()
// ArrayBuffer
const buffer = await request.arrayBuffer()
return NextResponse.json({ received: true })
}

URL 参数#

app/api/search/route.ts
import { NextRequest, NextResponse } from 'next/server'
// GET /api/search?q=keyword&page=1
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const query = searchParams.get('q')
const page = parseInt(searchParams.get('page') || '1')
const results = await db.post.findMany({
where: {
title: { contains: query || '' },
},
skip: (page - 1) * 10,
take: 10,
})
return NextResponse.json({
results,
page,
query,
})
}

请求头#

app/api/protected/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { headers } from 'next/headers'
export async function GET(request: NextRequest) {
// 方式一:从 request 获取
const authHeader = request.headers.get('authorization')
// 方式二:使用 headers() 函数
const headersList = await headers()
const userAgent = headersList.get('user-agent')
if (!authHeader?.startsWith('Bearer ')) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const token = authHeader.split(' ')[1]
// 验证 token...
return NextResponse.json({ authenticated: true })
}

Cookies#

app/api/auth/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { cookies } from 'next/headers'
export async function GET(request: NextRequest) {
// 方式一:从 request 获取
const token = request.cookies.get('token')?.value
// 方式二:使用 cookies() 函数
const cookieStore = await cookies()
const session = cookieStore.get('session')
return NextResponse.json({ token, session: session?.value })
}
export async function POST(request: NextRequest) {
const cookieStore = await cookies()
// 设置 cookie
cookieStore.set('token', 'abc123', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 天
})
return NextResponse.json({ success: true })
}
export async function DELETE() {
const cookieStore = await cookies()
// 删除 cookie
cookieStore.delete('token')
return NextResponse.json({ success: true })
}

响应处理#

JSON 响应#

app/api/data/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
// 基本 JSON 响应
return NextResponse.json({ name: 'Next.js' })
// 带状态码
return NextResponse.json({ error: 'Not found' }, { status: 404 })
// 带自定义头
return NextResponse.json(
{ data: 'value' },
{
status: 200,
headers: {
'X-Custom-Header': 'custom-value',
},
}
)
}

重定向#

app/api/redirect/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { redirect } from 'next/navigation'
export async function GET(request: NextRequest) {
// 方式一:NextResponse.redirect
return NextResponse.redirect(new URL('/dashboard', request.url))
// 方式二:redirect 函数
redirect('/dashboard')
}

设置响应头#

app/api/cors/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET() {
const response = NextResponse.json({ data: 'value' })
// 设置 CORS 头
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type')
return response
}
// 处理 OPTIONS 预检请求
export async function OPTIONS() {
return new NextResponse(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}

流式响应#

文本流#

app/api/stream/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
const messages = ['Hello', ' ', 'World', '!']
for (const message of messages) {
controller.enqueue(encoder.encode(message))
await new Promise((resolve) => setTimeout(resolve, 500))
}
controller.close()
},
})
return new NextResponse(stream, {
headers: {
'Content-Type': 'text/plain',
'Transfer-Encoding': 'chunked',
},
})
}

Server-Sent Events#

app/api/events/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
let count = 0
const interval = setInterval(() => {
count++
const data = JSON.stringify({ count, time: new Date().toISOString() })
controller.enqueue(encoder.encode(`data: ${data}\n\n`))
if (count >= 10) {
clearInterval(interval)
controller.close()
}
}, 1000)
},
})
return new NextResponse(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
})
}

客户端使用:

components/EventStream.tsx
'use client'
import { useEffect, useState } from 'react'
export function EventStream() {
const [events, setEvents] = useState<string[]>([])
useEffect(() => {
const eventSource = new EventSource('/api/events')
eventSource.onmessage = (event) => {
setEvents((prev) => [...prev, event.data])
}
eventSource.onerror = () => {
eventSource.close()
}
return () => eventSource.close()
}, [])
return (
<ul>
{events.map((event, i) => (
<li key={i}>{event}</li>
))}
</ul>
)
}

AI 流式响应#

app/api/chat/route.ts
import { NextRequest } from 'next/server'
export async function POST(request: NextRequest) {
const { messages } = await request.json()
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: 'gpt-4',
messages,
stream: true,
}),
})
// 直接转发流
return new Response(response.body, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
},
})
}

文件处理#

文件上传#

app/api/upload/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { writeFile } from 'fs/promises'
import { join } from 'path'
export async function POST(request: NextRequest) {
const formData = await request.formData()
const file = formData.get('file') as File
if (!file) {
return NextResponse.json({ error: 'No file uploaded' }, { status: 400 })
}
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
const filename = `${Date.now()}-${file.name}`
const path = join(process.cwd(), 'public', 'uploads', filename)
await writeFile(path, buffer)
return NextResponse.json({
url: `/uploads/${filename}`,
size: file.size,
type: file.type,
})
}

文件下载#

app/api/download/[filename]/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { readFile } from 'fs/promises'
import { join } from 'path'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ filename: string }> }
) {
const { filename } = await params
const path = join(process.cwd(), 'files', filename)
try {
const file = await readFile(path)
return new NextResponse(file, {
headers: {
'Content-Type': 'application/octet-stream',
'Content-Disposition': `attachment; filename="${filename}"`,
},
})
} catch {
return NextResponse.json({ error: 'File not found' }, { status: 404 })
}
}

缓存与重验证#

默认缓存行为#

app/api/time/route.ts
import { NextResponse } from 'next/server'
// GET 请求默认会被缓存(如果没有使用动态函数)
export async function GET() {
return NextResponse.json({ time: new Date().toISOString() })
}

禁用缓存#

app/api/realtime/route.ts
import { NextResponse } from 'next/server'
// 方式一:导出 dynamic 配置
export const dynamic = 'force-dynamic'
export async function GET() {
return NextResponse.json({ time: new Date().toISOString() })
}
// 方式二:设置 revalidate
export const revalidate = 0
// 方式三:使用动态函数
import { headers } from 'next/headers'
export async function GET() {
const headersList = await headers() // 触发动态渲染
return NextResponse.json({ time: new Date().toISOString() })
}

定时重验证#

app/api/news/route.ts
import { NextResponse } from 'next/server'
// 每 60 秒重新验证
export const revalidate = 60
export async function GET() {
const news = await fetch('https://api.example.com/news').then((r) => r.json())
return NextResponse.json(news)
}

认证与授权#

Bearer Token 认证#

app/api/protected/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { verifyToken } from '@/lib/auth'
export async function GET(request: NextRequest) {
const authHeader = request.headers.get('authorization')
if (!authHeader?.startsWith('Bearer ')) {
return NextResponse.json(
{ error: 'Missing authorization header' },
{ status: 401 }
)
}
const token = authHeader.split(' ')[1]
try {
const payload = await verifyToken(token)
return NextResponse.json({ user: payload })
} catch {
return NextResponse.json({ error: 'Invalid token' }, { status: 401 })
}
}

API Key 认证#

app/api/v1/[...path]/route.ts
import { NextRequest, NextResponse } from 'next/server'
const API_KEYS = new Set(process.env.API_KEYS?.split(',') || [])
function validateApiKey(request: NextRequest) {
const apiKey = request.headers.get('x-api-key')
return apiKey && API_KEYS.has(apiKey)
}
export async function GET(request: NextRequest) {
if (!validateApiKey(request)) {
return NextResponse.json({ error: 'Invalid API key' }, { status: 403 })
}
// 处理请求...
return NextResponse.json({ data: 'protected' })
}

Webhook 处理#

GitHub Webhook#

app/api/webhooks/github/route.ts
import { NextRequest, NextResponse } from 'next/server'
import crypto from 'crypto'
function verifySignature(payload: string, signature: string) {
const secret = process.env.GITHUB_WEBHOOK_SECRET!
const expectedSignature = `sha256=${crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex')}`
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)
}
export async function POST(request: NextRequest) {
const payload = await request.text()
const signature = request.headers.get('x-hub-signature-256')
if (!signature || !verifySignature(payload, signature)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
}
const event = request.headers.get('x-github-event')
const data = JSON.parse(payload)
switch (event) {
case 'push':
console.log('Push event:', data.ref)
// 处理推送事件
break
case 'pull_request':
console.log('PR event:', data.action)
// 处理 PR 事件
break
}
return NextResponse.json({ received: true })
}

Stripe Webhook#

app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!
export async function POST(request: NextRequest) {
const payload = await request.text()
const signature = request.headers.get('stripe-signature')!
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(payload, signature, webhookSecret)
} catch (err) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
}
switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object as Stripe.Checkout.Session
await handleCheckoutComplete(session)
break
case 'invoice.paid':
const invoice = event.data.object as Stripe.Invoice
await handleInvoicePaid(invoice)
break
}
return NextResponse.json({ received: true })
}
async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
// 处理支付完成
}
async function handleInvoicePaid(invoice: Stripe.Invoice) {
// 处理发票支付
}

实战示例#

RESTful API#

app/api/v1/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { db } from '@/lib/db'
const CreateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
})
export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '10')
const [users, total] = await Promise.all([
db.user.findMany({
skip: (page - 1) * limit,
take: limit,
select: { id: true, name: true, email: true, createdAt: true },
}),
db.user.count(),
])
return NextResponse.json({
data: users,
meta: {
page,
limit,
total,
pages: Math.ceil(total / limit),
},
})
}
export async function POST(request: NextRequest) {
const body = await request.json()
const result = CreateUserSchema.safeParse(body)
if (!result.success) {
return NextResponse.json(
{ errors: result.error.flatten().fieldErrors },
{ status: 400 }
)
}
const existing = await db.user.findUnique({
where: { email: result.data.email },
})
if (existing) {
return NextResponse.json({ error: 'Email already exists' }, { status: 409 })
}
const user = await db.user.create({
data: result.data,
})
return NextResponse.json(user, { status: 201 })
}
app/api/v1/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { db } from '@/lib/db'
const UpdateUserSchema = z.object({
name: z.string().min(1).max(100).optional(),
email: z.string().email().optional(),
})
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const user = await db.user.findUnique({
where: { id },
select: { id: true, name: true, email: true, createdAt: true },
})
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
return NextResponse.json(user)
}
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const body = await request.json()
const result = UpdateUserSchema.safeParse(body)
if (!result.success) {
return NextResponse.json(
{ errors: result.error.flatten().fieldErrors },
{ status: 400 }
)
}
try {
const user = await db.user.update({
where: { id },
data: result.data,
})
return NextResponse.json(user)
} catch {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
try {
await db.user.delete({ where: { id } })
return new NextResponse(null, { status: 204 })
} catch {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
}

常见问题#

🤔 Q: Route Handlers 和 Server Actions 怎么选?

🤔 Q: 如何处理跨域请求?

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const response = NextResponse.next()
if (request.nextUrl.pathname.startsWith('/api/')) {
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
}
return response
}

🤔 Q: 如何限制请求频率?

使用中间件或第三方库如 @upstash/ratelimit 实现速率限制。


下一篇将介绍数据库集成,学习如何在 Next.js 中使用 Prisma 和 Drizzle。

-EOF-