本章目标:掌握 Next.js App Router 中 Route Handlers 的完整使用方式——从 HTTP 方法映射、请求/响应对象、文件上传、流式 Body,到 CORS 配置、鉴权中间层,最终构建一个完整的 REST API。
8.1 Route Handlers 概述
什么是 Route Handler?
Route Handler 是 Next.js App Router 中用于处理 HTTP 请求的服务端函数。它类似于传统后端框架(Express / Fastify)中的路由,但深度集成了 Next.js 的缓存、流式响应和 Edge Runtime。
app/
└── api/
└── users/
└── route.ts ← Route Handler 文件
与 Pages Router API Routes 的区别
| 特性 | Pages Router (pages/api/*) | App Router (app/api/*/route.ts) |
|---|---|---|
| 文件命名 | pages/api/users.ts | app/api/users/route.ts |
| 函数签名 | export default function handler(req, res) | export async function GET(request: NextRequest) |
| HTTP 方法 | 同一个函数内 switch (req.method) | 每个方法导出为独立函数 |
| 流式响应 | 需要手动处理 | 原生支持 ReadableStream |
| Edge Runtime | 需要额外配置 | export const runtime = 'edge' |
| 缓存控制 | 手动设置 headers | export const dynamic = 'force-static' |
| TypeScript | NextApiRequest / NextApiResponse | NextRequest / NextResponse |
Route Handler 的本质
Route Handler 本质上是一个 HTTP 端点,它:
- 运行在 Node.js 环境(默认)或 Edge Runtime
- 返回标准的 Response 对象(兼容 Web Fetch API)
- 可以与 Server Components 和 Server Actions 协同工作
- 支持 流式响应 和 增量渲染
8.2 HTTP 方法实现
基础示例:CRUD 操作
// app/api/articles/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
// GET /api/articles - 获取文章列表
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get('page')) || 1;
const limit = Number(searchParams.get('limit')) || 10;
const category = searchParams.get('category');
const skip = (page - 1) * limit;
const [articles, total] = await Promise.all([
prisma.article.findMany({
where: category ? { category } : undefined,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
select: {
id: true,
title: true,
slug: true,
excerpt: true,
category: true,
createdAt: true,
author: {
select: { name: true, avatar: true },
},
},
}),
prisma.article.count({ where: category ? { category } : undefined }),
]);
return NextResponse.json({
data: articles,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
}
// POST /api/articles - 创建文章
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { title, slug, content, excerpt, category, tags } = body;
// 验证必填字段
if (!title || !slug || !content) {
return NextResponse.json(
{ error: 'Missing required fields: title, slug, content' },
{ status: 400 }
);
}
const article = await prisma.article.create({
data: {
title,
slug,
content,
excerpt,
category,
tags,
},
});
return NextResponse.json(article, { status: 201 });
} catch (error) {
console.error('Failed to create article:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
动态路由:单个资源操作
// app/api/articles/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
// 路由参数类型
type Params = { params: Promise<{ id: string }> };
// GET /api/articles/:id - 获取单篇文章
export async function GET(request: NextRequest, { params }: Params) {
const { id } = await params;
const article = await prisma.article.findUnique({
where: { id },
include: {
author: { select: { id: true, name: true, avatar: true } },
comments: {
orderBy: { createdAt: 'desc' },
take: 10,
},
},
});
if (!article) {
return NextResponse.json(
{ error: 'Article not found' },
{ status: 404 }
);
}
return NextResponse.json(article);
}
// PUT /api/articles/:id - 更新文章
export async function PUT(request: NextRequest, { params }: Params) {
const { id } = await params;
try {
const body = await request.json();
// 检查文章是否存在
const existing = await prisma.article.findUnique({ where: { id } });
if (!existing) {
return NextResponse.json(
{ error: 'Article not found' },
{ status: 404 }
);
}
const article = await prisma.article.update({
where: { id },
data: {
title: body.title,
slug: body.slug,
content: body.content,
excerpt: body.excerpt,
category: body.category,
tags: body.tags,
updatedAt: new Date(),
},
});
return NextResponse.json(article);
} catch (error) {
console.error('Failed to update article:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
// DELETE /api/articles/:id - 删除文章
export async function DELETE(request: NextRequest, { params }: Params) {
const { id } = await params;
try {
const existing = await prisma.article.findUnique({ where: { id } });
if (!existing) {
return NextResponse.json(
{ error: 'Article not found' },
{ status: 404 }
);
}
await prisma.article.delete({ where: { id } });
return NextResponse.json({ message: 'Article deleted successfully' });
} catch (error) {
console.error('Failed to delete article:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
// PATCH /api/articles/:id - 部分更新
export async function PATCH(request: NextRequest, { params }: Params) {
const { id } = await params;
try {
const body = await request.json();
// 只更新提供的字段
const allowedFields = ['title', 'slug', 'content', 'excerpt', 'category', 'tags', 'published'];
const data: Record<string, unknown> = {};
for (const field of allowedFields) {
if (body[field] !== undefined) {
data[field] = body[field];
}
}
data.updatedAt = new Date();
const article = await prisma.article.update({
where: { id },
data,
});
return NextResponse.json(article);
} catch (error) {
console.error('Failed to patch article:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
支持的 HTTP 方法
Next.js Route Handlers 支持以下 HTTP 方法:
// app/api/example/route.ts
export async function GET(request: NextRequest) { }
export async function HEAD(request: NextRequest) { }
export async function POST(request: NextRequest) { }
export async function PUT(request: NextRequest) { }
export async function PATCH(request: NextRequest) { }
export async function DELETE(request: NextRequest) { }
// 如果 GET 存在,HEAD 会自动复用 GET 的逻辑(除非显式导出 HEAD)
// OPTIONS 由 Next.js 自动生成(用于 CORS 预检)
8.3 NextRequest 与 NextResponse
NextRequest 详解
NextRequest 继承自 Web Fetch API 的 Request,并扩展了以下实用属性:
// app/api/debug/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
// 1. URL 参数
const { searchParams } = new URL(request.url);
const query = searchParams.get('q');
// 2. Cookies
const token = request.cookies.get('auth-token');
const allCookies = request.cookies.getAll();
// 3. Headers
const userAgent = request.headers.get('user-agent');
const contentType = request.headers.get('content-type');
// 4. 地理位置(Vercel Edge Network 自动注入)
const country = request.geo?.country;
const city = request.geo?.city;
const region = request.geo?.region;
// 5. IP 地址
const ip = request.ip;
// 6. 请求体(POST/PUT/PATCH)
// const body = await request.json();
// const text = await request.text();
// const formData = await request.formData();
// const blob = await request.blob();
// const arrayBuffer = await request.arrayBuffer();
return NextResponse.json({
url: request.url,
method: request.method,
query,
cookies: allCookies,
headers: {
userAgent,
contentType,
},
geo: { country, city, region },
ip,
});
}
NextResponse 详解
NextResponse 继承自 Web Fetch API 的 Response,提供以下便捷方法:
// app/api/response-examples/route.ts
import { NextRequest, NextResponse } from 'next/server';
// 1. JSON 响应
export async function GET(request: NextRequest) {
return NextResponse.json(
{ message: 'Hello, World!' },
{
status: 200,
headers: {
'X-Custom-Header': 'custom-value',
},
}
);
}
// 2. 重定向
export async function POST(request: NextRequest) {
return NextResponse.redirect(
new URL('/dashboard', request.url),
302
);
}
// 3. URL 重写(不改变浏览器地址栏)
export async function PUT(request: NextRequest) {
return NextResponse.rewrite(
new URL('/api/internal/data', request.url)
);
}
// 4. 设置 Cookies
export async function PATCH(request: NextRequest) {
const response = NextResponse.json({ success: true });
response.cookies.set('session-id', 'abc123', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 天
path: '/',
});
// 删除 Cookie
response.cookies.delete('old-cookie');
return response;
}
// 5. 流式响应
export async function DELETE(request: NextRequest) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 5; i++) {
controller.enqueue(encoder.encode(`Chunk ${i}\n`));
await new Promise(resolve => setTimeout(resolve, 1000));
}
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/plain',
'Transfer-Encoding': 'chunked',
},
});
}
8.4 请求体验证
手动验证
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
// 定义验证 Schema
const createUserSchema = z.object({
email: z.string().email('Invalid email format'),
name: z.string().min(2, 'Name must be at least 2 characters'),
password: z.string().min(8, 'Password must be at least 8 characters'),
role: z.enum(['admin', 'user']).default('user'),
});
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// 验证请求体
const validation = createUserSchema.safeParse(body);
if (!validation.success) {
return NextResponse.json(
{
error: 'Validation failed',
details: validation.error.errors.map(err => ({
field: err.path.join('.'),
message: err.message,
})),
},
{ status: 422 }
);
}
// 验证通过,使用类型安全的数据
const { email, name, password, role } = validation.data;
// 创建用户...
const user = await createUser({ email, name, password, role });
return NextResponse.json(user, { status: 201 });
} catch (error) {
if (error instanceof SyntaxError) {
return NextResponse.json(
{ error: 'Invalid JSON in request body' },
{ status: 400 }
);
}
throw error;
}
}
封装验证工具
// lib/validate.ts
import { NextRequest, NextResponse } from 'next/server';
import { ZodSchema, ZodError } from 'zod';
export async function validateRequest<T>(
request: NextRequest,
schema: ZodSchema<T>
): Promise<{ data: T } | { error: NextResponse }> {
try {
const body = await request.json();
const data = schema.parse(body);
return { data };
} catch (error) {
if (error instanceof ZodError) {
return {
error: NextResponse.json(
{
error: 'Validation failed',
details: error.errors.map(err => ({
field: err.path.join('.'),
message: err.message,
})),
},
{ status: 422 }
),
};
}
return {
error: NextResponse.json(
{ error: 'Invalid request body' },
{ status: 400 }
),
};
}
}
使用方式:
// app/api/users/route.ts
import { validateRequest } from '@/lib/validate';
import { createUserSchema } from '@/lib/schemas';
export async function POST(request: NextRequest) {
const result = await validateRequest(request, createUserSchema);
if ('error' in result) {
return result.error;
}
// result.data 是类型安全的
const { email, name, password, role } = result.data;
// 创建用户...
}
8.5 文件上传
单文件上传
// 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) {
try {
const formData = await request.formData();
const file = formData.get('file') as File | null;
if (!file) {
return NextResponse.json(
{ error: 'No file provided' },
{ status: 400 }
);
}
// 验证文件类型
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
if (!allowedTypes.includes(file.type)) {
return NextResponse.json(
{ error: 'Invalid file type. Only JPEG, PNG, WebP, and GIF are allowed.' },
{ status: 400 }
);
}
// 验证文件大小(5MB)
const maxSize = 5 * 1024 * 1024;
if (file.size > maxSize) {
return NextResponse.json(
{ error: 'File too large. Maximum size is 5MB.' },
{ status: 400 }
);
}
// 读取文件内容
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
// 生成唯一文件名
const ext = file.name.split('.').pop();
const filename = `${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`;
// 保存到 public/uploads/
const uploadDir = join(process.cwd(), 'public', 'uploads');
const filePath = join(uploadDir, filename);
await writeFile(filePath, buffer);
return NextResponse.json({
success: true,
url: `/uploads/${filename}`,
filename,
size: file.size,
type: file.type,
});
} catch (error) {
console.error('Upload failed:', error);
return NextResponse.json(
{ error: 'Upload failed' },
{ status: 500 }
);
}
}
多文件上传
// app/api/upload-multiple/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { writeFile } from 'fs/promises';
import { join } from 'path';
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const MAX_FILES = 10;
export async function POST(request: NextRequest) {
try {
const formData = await request.formData();
const files = formData.getAll('files') as File[];
if (files.length === 0) {
return NextResponse.json(
{ error: 'No files provided' },
{ status: 400 }
);
}
if (files.length > MAX_FILES) {
return NextResponse.json(
{ error: `Too many files. Maximum is ${MAX_FILES}.` },
{ status: 400 }
);
}
const uploadDir = join(process.cwd(), 'public', 'uploads');
const results: Array<{ url: string; filename: string; size: number }> = [];
const errors: Array<{ filename: string; error: string }> = [];
for (const file of files) {
try {
if (!ALLOWED_TYPES.includes(file.type)) {
errors.push({
filename: file.name,
error: 'Invalid file type',
});
continue;
}
if (file.size > MAX_FILE_SIZE) {
errors.push({
filename: file.name,
error: 'File too large',
});
continue;
}
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
const ext = file.name.split('.').pop();
const filename = `${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`;
const filePath = join(uploadDir, filename);
await writeFile(filePath, buffer);
results.push({
url: `/uploads/${filename}`,
filename,
size: file.size,
});
} catch (error) {
errors.push({
filename: file.name,
error: 'Upload failed',
});
}
}
return NextResponse.json({
success: results.length > 0,
uploaded: results,
errors: errors.length > 0 ? errors : undefined,
});
} catch (error) {
console.error('Upload failed:', error);
return NextResponse.json(
{ error: 'Upload failed' },
{ status: 500 }
);
}
}
流式文件上传(大文件)
// app/api/upload-stream/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createWriteStream } from 'fs';
import { join } from 'path';
import { mkdir } from 'fs/promises';
export async function POST(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const filename = searchParams.get('filename');
if (!filename) {
return NextResponse.json(
{ error: 'Missing filename parameter' },
{ status: 400 }
);
}
// 确保上传目录存在
const uploadDir = join(process.cwd(), 'uploads');
await mkdir(uploadDir, { recursive: true });
const filePath = join(uploadDir, filename);
// 创建写入流
const writeStream = createWriteStream(filePath);
// 获取请求体的 ReadableStream
const body = request.body;
if (!body) {
return NextResponse.json(
{ error: 'No body provided' },
{ status: 400 }
);
}
const reader = body.getReader();
let totalBytes = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
writeStream.write(value);
totalBytes += value.length;
}
writeStream.end();
// 等待写入完成
await new Promise<void>((resolve, reject) => {
writeStream.on('finish', resolve);
writeStream.on('error', reject);
});
return NextResponse.json({
success: true,
filename,
size: totalBytes,
});
} catch (error) {
console.error('Stream upload failed:', error);
return NextResponse.json(
{ error: 'Upload failed' },
{ status: 500 }
);
}
}
客户端使用:
// app/components/UploadButton.tsx
'use client';
import { useState } from 'react';
export function UploadButton() {
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const handleUpload = async (file: File) => {
setUploading(true);
setProgress(0);
try {
const response = await fetch(
`/api/upload-stream?filename=${encodeURIComponent(file.name)}`,
{
method: 'POST',
body: file,
}
);
if (!response.ok) {
throw new Error('Upload failed');
}
const result = await response.json();
console.log('Upload complete:', result);
} catch (error) {
console.error('Upload error:', error);
} finally {
setUploading(false);
}
};
return (
<input
type="file"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleUpload(file);
}}
disabled={uploading}
/>
);
}
8.6 流式响应
Server-Sent Events (SSE)
// app/api/events/route.ts
import { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
// 发送初始连接事件
controller.enqueue(
encoder.encode('event: connected\ndata: {"status":"connected"}\n\n')
);
// 模拟实时数据推送
let count = 0;
const interval = setInterval(() => {
if (count >= 10) {
clearInterval(interval);
controller.close();
return;
}
const data = {
count,
timestamp: new Date().toISOString(),
message: `Update ${count}`,
};
controller.enqueue(
encoder.encode(`event: update\ndata: ${JSON.stringify(data)}\n\n`)
);
count++;
}, 1000);
// 处理客户端断开连接
request.signal.addEventListener('abort', () => {
clearInterval(interval);
controller.close();
});
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
客户端使用:
// app/components/EventStream.tsx
'use client';
import { useEffect, useState } from 'react';
export function EventStream() {
const [events, setEvents] = useState<Array<{ count: number; message: string }>>([]);
useEffect(() => {
const eventSource = new EventSource('/api/events');
eventSource.addEventListener('update', (event) => {
const data = JSON.parse(event.data);
setEvents(prev => [...prev, data]);
});
eventSource.addEventListener('connected', () => {
console.log('Connected to event stream');
});
eventSource.onerror = () => {
console.error('EventSource error');
eventSource.close();
};
return () => {
eventSource.close();
};
}, []);
return (
<div>
<h2>Live Events</h2>
<ul>
{events.map((event, i) => (
<li key={i}>{event.message}</li>
))}
</ul>
</div>
);
}
流式 JSON(增量渲染)
// app/api/stream-json/route.ts
import { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
// 开始 JSON 数组
controller.enqueue(encoder.encode('['));
const items = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
];
for (let i = 0; i < items.length; i++) {
const prefix = i > 0 ? ',' : '';
controller.enqueue(encoder.encode(prefix + JSON.stringify(items[i])));
// 模拟延迟
await new Promise(resolve => setTimeout(resolve, 500));
}
// 结束 JSON 数组
controller.enqueue(encoder.encode(']'));
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'application/json',
'Transfer-Encoding': 'chunked',
},
});
}
8.7 安全加固
CORS 配置
// app/api/cors-example/route.ts
import { NextRequest, NextResponse } from 'next/server';
// CORS 配置
const ALLOWED_ORIGINS = [
'http://localhost:3000',
'https://yourdomain.com',
];
function corsHeaders(origin: string | null) {
const isAllowed = origin && ALLOWED_ORIGINS.includes(origin);
return {
'Access-Control-Allow-Origin': isAllowed ? origin : ALLOWED_ORIGINS[0],
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400', // 24 小时
};
}
// 处理预检请求
export async function OPTIONS(request: NextRequest) {
const origin = request.headers.get('origin');
return new NextResponse(null, {
status: 204,
headers: corsHeaders(origin),
});
}
export async function GET(request: NextRequest) {
const origin = request.headers.get('origin');
return NextResponse.json(
{ message: 'Hello with CORS' },
{ headers: corsHeaders(origin) }
);
}
export async function POST(request: NextRequest) {
const origin = request.headers.get('origin');
const body = await request.json();
return NextResponse.json(
{ received: body },
{ headers: corsHeaders(origin) }
);
}
鉴权中间层
// lib/auth-middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { verify } from 'jsonwebtoken';
export interface AuthUser {
id: string;
email: string;
role: 'admin' | 'user';
}
export async function authenticate(
request: NextRequest
): Promise<AuthUser | null> {
const authHeader = request.headers.get('authorization');
if (!authHeader?.startsWith('Bearer ')) {
return null;
}
const token = authHeader.substring(7);
try {
const decoded = verify(token, process.env.JWT_SECRET!) as AuthUser;
return decoded;
} catch (error) {
return null;
}
}
// 鉴权装饰器
export function withAuth<T extends unknown[]>(
handler: (request: NextRequest, user: AuthUser, ...args: T) => Promise<NextResponse>
) {
return async (request: NextRequest, ...args: T) => {
const user = await authenticate(request);
if (!user) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
return handler(request, user, ...args);
};
}
// 角色检查
export function withRole<T extends unknown[]>(
role: AuthUser['role'],
handler: (request: NextRequest, user: AuthUser, ...args: T) => Promise<NextResponse>
) {
return withAuth(async (request, user, ...args) => {
if (user.role !== role) {
return NextResponse.json(
{ error: 'Forbidden' },
{ status: 403 }
);
}
return handler(request, user, ...args);
});
}
使用方式:
// app/api/protected/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { withAuth, withRole, AuthUser } from '@/lib/auth-middleware';
// 需要登录
export const GET = withAuth(async (request, user) => {
return NextResponse.json({
message: `Hello, ${user.email}!`,
user,
});
});
// 需要管理员角色
export const POST = withRole('admin', async (request, user) => {
const body = await request.json();
// 只有管理员可以执行此操作
return NextResponse.json({
message: 'Admin action performed',
performedBy: user.email,
});
});
速率限制
// lib/rate-limit.ts
import { NextRequest, NextResponse } from 'next/server';
interface RateLimitEntry {
count: number;
resetAt: number;
}
const rateLimitMap = new Map<string, RateLimitEntry>();
export function rateLimit(
request: NextRequest,
options: {
windowMs: number;
maxRequests: number;
} = { windowMs: 60000, maxRequests: 100 }
): { success: boolean; response?: NextResponse } {
const ip = request.ip || request.headers.get('x-forwarded-for') || 'unknown';
const now = Date.now();
const entry = rateLimitMap.get(ip);
if (!entry || now > entry.resetAt) {
// 新窗口
rateLimitMap.set(ip, {
count: 1,
resetAt: now + options.windowMs,
});
return { success: true };
}
if (entry.count >= options.maxRequests) {
// 超过限制
const retryAfter = Math.ceil((entry.resetAt - now) / 1000);
return {
success: false,
response: NextResponse.json(
{ error: 'Too many requests' },
{
status: 429,
headers: {
'Retry-After': retryAfter.toString(),
'X-RateLimit-Limit': options.maxRequests.toString(),
'X-RateLimit-Remaining': '0',
'X-RateLimit-Reset': entry.resetAt.toString(),
},
}
),
};
}
// 增加计数
entry.count++;
rateLimitMap.set(ip, entry);
return {
success: true,
// 可以添加 headers 显示剩余次数
};
}
使用方式:
// app/api/login/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { rateLimit } from '@/lib/rate-limit';
export async function POST(request: NextRequest) {
// 速率限制:每分钟最多 5 次
const { success, response } = rateLimit(request, {
windowMs: 60000,
maxRequests: 5,
});
if (!success) {
return response!;
}
const body = await request.json();
const { email, password } = body;
// 验证登录...
return NextResponse.json({ token: 'jwt-token' });
}
8.8 路由配置导出
// app/api/example/route.ts
// 1. 运行时选择
export const runtime = 'nodejs'; // 'nodejs' | 'edge'
// 2. 动态渲染策略
export const dynamic = 'auto';
// 'auto' | 'force-dynamic' | 'error' | 'force-static'
// 3. 重新验证时间(ISR)
export const revalidate = 60; // 秒,false = 永久缓存
// 4. 路由段配置
export const fetchCache = 'auto';
// 'auto' | 'default-cache' | 'only-cache' | 'force-cache' | 'no-cache'
export const preferredRegion = 'iad1'; // 首选区域
export const maxDuration = 30; // 最大执行时间(秒)
Edge Runtime 示例
// app/api/geo/route.ts
import { NextRequest, NextResponse } from 'next/server';
export const runtime = 'edge';
export async function GET(request: NextRequest) {
const country = request.geo?.country || 'Unknown';
const city = request.geo?.city || 'Unknown';
const region = request.geo?.region || 'Unknown';
return NextResponse.json({
message: `You are in ${city}, ${region}, ${country}`,
geo: {
country,
city,
region,
},
ip: request.ip,
});
}
8.9 实战:完整 REST API
项目结构
app/
└── api/
└── v1/
├── articles/
│ ├── route.ts # GET (列表), POST (创建)
│ └── [id]/
│ ├── route.ts # GET, PUT, DELETE
│ └── comments/
│ └── route.ts # GET, POST
├── users/
│ ├── route.ts # GET, POST
│ ├── [id]/
│ │ └── route.ts # GET, PUT, DELETE
│ └── me/
│ └── route.ts # 当前用户
├── auth/
│ ├── login/
│ │ └── route.ts
│ ├── register/
│ │ └── route.ts
│ └── logout/
│ └── route.ts
└── upload/
└── route.ts
完整的文章 API
// app/api/v1/articles/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { withAuth, AuthUser } from '@/lib/auth-middleware';
import { validateRequest } from '@/lib/validate';
import { z } from 'zod';
const createArticleSchema = z.object({
title: z.string().min(1).max(200),
slug: z.string().regex(/^[a-z0-9-]+$/),
content: z.string().min(1),
excerpt: z.string().max(500).optional(),
category: z.string().optional(),
tags: z.array(z.string()).optional(),
published: z.boolean().default(false),
});
// GET /api/v1/articles - 获取文章列表
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
// 分页
const page = Math.max(1, Number(searchParams.get('page')) || 1);
const limit = Math.min(100, Math.max(1, Number(searchParams.get('limit')) || 20));
const skip = (page - 1) * limit;
// 筛选
const category = searchParams.get('category');
const tag = searchParams.get('tag');
const author = searchParams.get('author');
const published = searchParams.get('published');
// 搜索
const search = searchParams.get('q');
// 排序
const sortBy = searchParams.get('sortBy') || 'createdAt';
const sortOrder = (searchParams.get('sortOrder') || 'desc') as 'asc' | 'desc';
// 构建查询条件
const where: any = {};
if (category) where.category = category;
if (tag) where.tags = { has: tag };
if (author) where.author = { id: author };
if (published === 'true') where.published = true;
if (published === 'false') where.published = false;
if (search) {
where.OR = [
{ title: { contains: search, mode: 'insensitive' } },
{ content: { contains: search, mode: 'insensitive' } },
];
}
try {
const [articles, total] = await Promise.all([
prisma.article.findMany({
where,
skip,
take: limit,
orderBy: { [sortBy]: sortOrder },
select: {
id: true,
title: true,
slug: true,
excerpt: true,
category: true,
tags: true,
published: true,
createdAt: true,
updatedAt: true,
author: {
select: {
id: true,
name: true,
avatar: true,
},
},
_count: {
select: {
comments: true,
likes: true,
},
},
},
}),
prisma.article.count({ where }),
]);
return NextResponse.json({
data: articles,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
hasNext: page * limit < total,
hasPrev: page > 1,
},
});
} catch (error) {
console.error('Failed to fetch articles:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
// POST /api/v1/articles - 创建文章(需要登录)
export const POST = withAuth(async (request: NextRequest, user: AuthUser) => {
const result = await validateRequest(request, createArticleSchema);
if ('error' in result) {
return result.error;
}
const data = result.data;
try {
// 检查 slug 是否已存在
const existing = await prisma.article.findUnique({
where: { slug: data.slug },
});
if (existing) {
return NextResponse.json(
{ error: 'Slug already exists' },
{ status: 409 }
);
}
const article = await prisma.article.create({
data: {
...data,
authorId: user.id,
},
include: {
author: {
select: {
id: true,
name: true,
avatar: true,
},
},
},
});
return NextResponse.json(article, { status: 201 });
} catch (error) {
console.error('Failed to create article:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
});
单篇文章操作
// app/api/v1/articles/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { withAuth, AuthUser } from '@/lib/auth-middleware';
type Params = { params: Promise<{ id: string }> };
// GET /api/v1/articles/:id
export async function GET(request: NextRequest, { params }: Params) {
const { id } = await params;
try {
const article = await prisma.article.findUnique({
where: { id },
include: {
author: {
select: {
id: true,
name: true,
avatar: true,
bio: true,
},
},
comments: {
orderBy: { createdAt: 'desc' },
take: 20,
include: {
author: {
select: {
id: true,
name: true,
avatar: true,
},
},
},
},
_count: {
select: {
comments: true,
likes: true,
},
},
},
});
if (!article) {
return NextResponse.json(
{ error: 'Article not found' },
{ status: 404 }
);
}
// 增加浏览量
await prisma.article.update({
where: { id },
data: { views: { increment: 1 } },
});
return NextResponse.json(article);
} catch (error) {
console.error('Failed to fetch article:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
// PUT /api/v1/articles/:id - 更新文章(需要登录且是作者)
export const PUT = withAuth(async (
request: NextRequest,
user: AuthUser,
{ params }: Params
) => {
const { id } = await params;
try {
const existing = await prisma.article.findUnique({
where: { id },
select: { authorId: true },
});
if (!existing) {
return NextResponse.json(
{ error: 'Article not found' },
{ status: 404 }
);
}
// 检查权限:只有作者或管理员可以更新
if (existing.authorId !== user.id && user.role !== 'admin') {
return NextResponse.json(
{ error: 'Forbidden' },
{ status: 403 }
);
}
const body = await request.json();
const article = await prisma.article.update({
where: { id },
data: {
title: body.title,
slug: body.slug,
content: body.content,
excerpt: body.excerpt,
category: body.category,
tags: body.tags,
published: body.published,
updatedAt: new Date(),
},
include: {
author: {
select: {
id: true,
name: true,
avatar: true,
},
},
},
});
return NextResponse.json(article);
} catch (error) {
console.error('Failed to update article:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
});
// DELETE /api/v1/articles/:id - 删除文章(需要登录且是作者)
export const DELETE = withAuth(async (
request: NextRequest,
user: AuthUser,
{ params }: Params
) => {
const { id } = await params;
try {
const existing = await prisma.article.findUnique({
where: { id },
select: { authorId: true },
});
if (!existing) {
return NextResponse.json(
{ error: 'Article not found' },
{ status: 404 }
);
}
// 检查权限
if (existing.authorId !== user.id && user.role !== 'admin') {
return NextResponse.json(
{ error: 'Forbidden' },
{ status: 403 }
);
}
await prisma.article.delete({ where: { id } });
return NextResponse.json({
message: 'Article deleted successfully',
});
} catch (error) {
console.error('Failed to delete article:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
});
认证 API
// app/api/v1/auth/login/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { compare } from 'bcryptjs';
import { sign } from 'jsonwebtoken';
import { rateLimit } from '@/lib/rate-limit';
export async function POST(request: NextRequest) {
// 速率限制:每分钟最多 5 次
const { success, response } = rateLimit(request, {
windowMs: 60000,
maxRequests: 5,
});
if (!success) {
return response!;
}
try {
const { email, password } = await request.json();
if (!email || !password) {
return NextResponse.json(
{ error: 'Email and password are required' },
{ status: 400 }
);
}
// 查找用户
const user = await prisma.user.findUnique({
where: { email },
select: {
id: true,
email: true,
name: true,
role: true,
password: true,
},
});
if (!user) {
return NextResponse.json(
{ error: 'Invalid email or password' },
{ status: 401 }
);
}
// 验证密码
const isValid = await compare(password, user.password);
if (!isValid) {
return NextResponse.json(
{ error: 'Invalid email or password' },
{ status: 401 }
);
}
// 生成 JWT
const token = sign(
{
id: user.id,
email: user.email,
role: user.role,
},
process.env.JWT_SECRET!,
{ expiresIn: '7d' }
);
// 设置 HttpOnly Cookie
const res = NextResponse.json({
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
},
});
res.cookies.set('auth-token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 天
path: '/',
});
return res;
} catch (error) {
console.error('Login failed:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
本章小结
Key Takeaways
- Route Handlers 是 Next.js 的原生 API 解决方案:基于 Web 标准 API(Request/Response),支持所有 HTTP 方法
- NextRequest/NextResponse 提供了丰富的扩展:cookies、geo、IP、流式响应等
- 文件上传支持多种模式:formData(小文件)、流式上传(大文件)、多文件上传
- 安全加固是必须的:CORS、鉴权中间层、速率限制、输入验证
- 流式响应适用于实时场景:SSE、增量 JSON、大文件下载
- 路由配置导出控制运行时行为:runtime、dynamic、revalidate、preferredRegion
下一步
下一章我们将深入 Server Actions——另一种后端函数调用方式。与 Route Handlers 不同,Server Actions 可以直接从 Server Components 和 Client Components 调用,无需手动管理 HTTP 请求。我们将对比两者的适用场景,并学习如何在表单提交、乐观更新、错误处理中使用 Server Actions。
参考资料
- Next.js Route Handlers 官方文档
- NextRequest / NextResponse API
- Web Fetch API - Request
- Web Fetch API - Response
- ReadableStream API
- Server-Sent Events
- Zod Schema Validation
- JSON Web Tokens (JWT)
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。