API 与自定义服务 — CodeRoadMapAPI 与自定义服务
Route Handlers。
API 与自定义服务
这一课你将能做到
- 用
route.ts 创建 RESTful API 端点,支持 GET、POST、PUT、DELETE
- 用 Middleware 实现请求拦截、鉴权、重定向、CORS 处理
- 理解 Edge Runtime 和 Node.js Runtime 的区别,按需选择
- 处理文件上传、请求体解析、自定义 Header
- 区分 Route Handler 和 Server Action 的适用场景
为什么要学
Next.js 不只是前端框架,也是全栈框架。route.ts 让你在同一项目里写 API,和前端共享类型、共享工具函数、共享环境变量。Middleware 让你在请求到达页面之前做拦截:未登录用户重定向到登录页、API 请求校验 Token、A/B 测试分流。Edge Runtime 让你在离用户最近的节点执行代码,延迟从几百毫秒降到几十毫秒。
人话:传统架构是「前端 → Nginx → 后端 API → 数据库」,Next.js 可以是「前端 → Next.js API → 数据库」,甚至「边缘节点直接返回」——少一跳是一跳。
先建立图像
Route Handler 文件结构:
app/
└── api/
├── users/
│ ├── route.ts ← GET /api/users, POST /api/users
│ └── [id]/
│ └── route.ts ← GET /api/users/1, PUT /api/users/1
├── upload/
│ └── route.ts ← POST /api/upload
└── auth/
└── [...nextauth]/
└── route.ts ← NextAuth.js 端点
Middleware 执行位置:
请求 → middleware.ts → 匹配的路由 → route.ts / page.tsx
(鉴权/重定向/重写)
跟着做
步骤 1:基础 Route Handler
app/api/users/route.ts:
import { NextResponse } from 'next/server';
const users = [
{ id: 1, name: '张三', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', email: 'lisi@example.com' },
];
export async function GET() {
return NextResponse.json(users);
}
export async function POST(request: Request) {
const body = await request.json();
const newUser = {
id: users.length + 1,
name: body.name,
email: body.email,
};
users.push(newUser);
return NextResponse.json(newUser, { status: 201 });
}
测试:
curl http://localhost:3000/api/users
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{"name":"王五","email":"wangwu@example.com"}'
步骤 2:动态路由参数
app/api/users/[id]/route.ts:
import { NextResponse } from 'next/server';
const users = [
{ id: 1, name: '张三', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', email: 'lisi@example.com' },
];
interface Props {
params: Promise<{ id: string }>;
}
export async function GET(request: Request, { params }: Props) {
const { id } = await params;
const user = users.find(u => u.id === Number(id));
if (!user) {
return NextResponse.json({ error: '用户不存在' }, { status: 404 });
}
return NextResponse.json(user);
}
export async function PUT(request: Request, { params }: Props) {
const { id } = await params;
const body = await request.json();
const index = users.findIndex(u => u.id === Number(id));
if (index === -1) {
return NextResponse.json({ error: '用户不存在' }, { status: 404 });
}
users[index] = { ...users[index], ...body };
return NextResponse.json(users[index]);
}
export async function DELETE(request: Request, { params }: Props) {
const { id } = await params;
const index = users.findIndex(u => u.id === Number(id));
if (index === -1) {
return NextResponse.json({ error: '用户不存在' }, { status: 404 });
}
users.splice(index, 1);
return new NextResponse(null, { status: 204 });
}
步骤 3:Middleware 鉴权
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
const isAuthPage = request.nextUrl.pathname.startsWith('/login');
const isApiRoute = request.nextUrl.pathname.startsWith('/api');
const isProtectedPage = request.nextUrl.pathname.startsWith('/dashboard');
// API 路由鉴权
if (isApiRoute) {
const authHeader = request.headers.get('authorization');
if (!authHeader?.startsWith('Bearer ')) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
// 验证 token...
return NextResponse.next();
}
// 页面路由鉴权
if (isProtectedPage && !token) {
return NextResponse.redirect(new URL('/login', request.url));
}
if (isAuthPage && token) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/login', '/api/:path*'],
};
步骤 4:CORS 处理
app/api/users/route.ts 添加 CORS 头:
import { NextResponse } from 'next/server';
export async function GET() {
const data = { users: [] };
return NextResponse.json(data, {
headers: {
'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 NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
生产环境应该把 * 换成具体域名,如 https://your-frontend.com。
步骤 5:文件上传
import { NextResponse } from 'next/server';
import { writeFile } from 'fs/promises';
import path from 'path';
export async function POST(request: Request) {
const formData = await request.formData();
const file = formData.get('file') as File;
if (!file) {
return NextResponse.json({ error: '没有文件' }, { status: 400 });
}
// 限制文件类型
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
return NextResponse.json({ error: '只支持 JPG/PNG/WebP' }, { status: 400 });
}
// 限制文件大小 5MB
if (file.size > 5 * 1024 * 1024) {
return NextResponse.json({ error: '文件不能超过 5MB' }, { status: 400 });
}
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
// 保存到 public/uploads
const filename = `${Date.now()}-${file.name}`;
const uploadDir = path.join(process.cwd(), 'public/uploads');
await writeFile(path.join(uploadDir, filename), buffer);
return NextResponse.json({
url: `/uploads/${filename}`,
size: file.size,
type: file.type,
});
}
'use client';
import { useState } from 'react';
export default function UploadForm() {
const [uploading, setUploading] = useState(false);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setUploading(true);
const formData = new FormData(e.currentTarget);
const res = await fetch('/api/upload', {
method: 'POST',
body: formData,
});
const data = await res.json();
setUploading(false);
alert(JSON.stringify(data));
}
return (
<form onSubmit={handleSubmit}>
<input type="file" name="file" accept="image/*" required />
<button type="submit" disabled={uploading}>
{uploading ? '上传中…' : '上传'}
</button>
</form>
);
}
步骤 6:Edge Runtime
Edge Runtime 是轻量级 V8 环境,启动快、延迟低,但不支持 Node.js API(如 fs、path)。
export const runtime = 'edge';
export async function GET(request: Request) {
// 可以用 Web API: Request, Response, URL, TextEncoder 等
const { searchParams } = new URL(request.url);
const name = searchParams.get('name') || '世界';
return new Response(`你好,${name}!来自 Edge Runtime`, {
headers: { 'content-type': 'text/plain; charset=utf-8' },
});
}
Node.js Runtime(默认)支持完整 Node.js API,适合数据库连接、文件操作。Edge Runtime 适合:鉴权、重定向、A/B 测试、简单 API。
讲清楚要点
- Route Handler 是 App Router 的 API 层:替代 Pages Router 的
pages/api/。用 route.ts 定义,支持 GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS。
- Middleware 在请求前执行:适合做鉴权、重定向、重写、Header 修改。不能返回响应体,只能返回
NextResponse.next() 或 redirect() / rewrite()。
- Edge Runtime 是 V8 Isolate:启动时间 <1ms,内存占用小,全球边缘节点部署。但不支持 Node.js API,包体积有限制。
- Node.js Runtime 是完整 Node:支持所有 Node.js API,适合数据库连接、文件操作、复杂计算。默认 Runtime。
- Route Handler 和 Server Action 的区别:Route Handler 是显式 API 端点,适合对外暴露接口;Server Action 是隐式 POST 到当前页,适合内部表单提交。Route Handler 更通用,Server Action 更简洁。
- NextResponse 是扩展的 Response:支持
json()、redirect()、rewrite()、next(),比原生 Response 更方便。
你可能会懵的地方
-
Route Handler 和 Server Action 到底用哪个?
对外提供 API(给第三方调用、给移动端用)→ Route Handler;内部表单提交、数据变更 → Server Action。两者可以共存。
-
Middleware 能修改请求体吗?
不能。Middleware 只能读请求、改 Header、做重定向/重写。请求体处理在 Route Handler 里做。
-
Edge Runtime 能连数据库吗?
不能直接用 pg、mysql2 等 Node.js 驱动。可以用 HTTP 数据库(如 PlanetScale、Neon、Supabase REST API)或 Edge 兼容驱动。
-
export const runtime = 'edge' 放在哪?
放在 route.ts 或 page.tsx 顶部,控制该文件在 Edge 还是 Node.js 执行。middleware.ts 默认就是 Edge。
-
CORS 预检请求(OPTIONS)必须处理吗?
如果前端是跨域调用,且用了非简单请求(如 PUT/DELETE/自定义 Header),浏览器会先发 OPTIONS 预检。必须在 Route Handler 里处理 OPTIONS 方法。
错法对照
| 错法 | 问题 | 正解 |
|---|
Route Handler 里用 req.query | Pages Router 的 API,App Router 没有 | 用 new URL(request.url).searchParams |
| Middleware 里返回 JSON | Middleware 不能返回响应体 | 用 NextResponse.json() 或 redirect() |
Edge Runtime 里用 fs | 不支持 Node.js API | 切回 Node.js Runtime 或用 Edge 兼容方案 |
| 文件上传不限制大小 | 服务器被大文件撑爆 | 限制 file.size,或改用分片上传 |
CORS 配置 * 用于生产环境 | 安全风险,任意站点可调用 | 配置具体域名白名单 |
在 Route Handler 里用 redirect() | redirect() 是页面导航 API | API 里用 NextResponse.redirect() 或返回 307 |
卡点急救
- Route Handler 404:确认文件名是
route.ts 不是 page.ts;确认文件夹名对应 URL 路径;确认导出了 HTTP 方法函数(GET/POST 等)。
- Middleware 死循环:
redirect() 的目标路径又匹配了同一个 middleware。在 middleware 开头排除已重定向的路径。
- 文件上传报
formData.get 返回 null:确认前端 <input type="file" name="file"> 的 name 和后端 formData.get('file') 一致。
- Edge Runtime 报错
Module not found: Can't resolve 'fs':Edge 不支持 fs。把文件操作移到 Node.js Runtime,或改用对象存储(S3、R2)。
- CORS 预检失败:确认 OPTIONS 方法返回了正确的
Access-Control-Allow-* 头;确认 Access-Control-Allow-Headers 包含了前端实际发送的 Header。
面试常问
-
Route Handler 和 Server Action 的区别?
Route Handler 是显式 API 端点,URL 是 /api/xxx,适合对外暴露接口、Webhook、移动端调用;Server Action 是隐式 POST 到当前页,适合表单提交、内部数据变更。Route Handler 更通用,Server Action 更简洁、自动绑定到页面。
-
Middleware 的执行时机和限制?
在请求到达路由之前执行,可以读请求、改 Header、重定向/重写。不能返回响应体、不能修改请求体、不能访问 Node.js API(Edge Runtime)。适合做鉴权、A/B 测试、地域重定向。
-
Edge Runtime 和 Node.js Runtime 怎么选?
需要 Node.js API(fs、数据库驱动、加密库)→ Node.js Runtime;需要低延迟、全球分布、轻量逻辑(鉴权、重定向)→ Edge Runtime。Edge 启动快但功能受限,Node 功能全但冷启动慢。
-
怎么在 Next.js 里实现文件上传?
Route Handler 里用 request.formData() 解析 multipart 表单,拿到 File 对象后转 Buffer 保存到本地或对象存储。注意限制文件大小和类型,防止恶意上传。
-
Next.js 的 API 安全怎么保障?
① Middleware 统一鉴权;② Route Handler 里验证参数和权限;③ CORS 白名单;④ Rate Limiting(用 Redis 或内存计数);⑤ 输入校验(zod、yup);⑥ 环境变量隔离敏感配置。
自测三问
app/api/users/[id]/route.ts 里怎么拿到动态参数 id?
- Middleware 的
matcher 配置 ['/api/:path*'] 匹配哪些路径?/api 本身匹配吗?
- 什么场景下必须用 Route Handler 而不是 Server Action?
小结一句话
Route Handler 是 Next.js 的 API 层,Middleware 是请求的门卫,Edge Runtime 是轻量执行环境——三者组合,让 Next.js 从纯前端框架变成能写接口、能鉴权、能全球分发的全栈框架。
延伸