文章
Next.js Route Handler 代理模板
下面是一个 通用、安全、可复用的 Next.js Route Handler 代理模板,支持 GET
、POST
、PUT
、DELETE
等所有 HTTP 方法,并自动处理:
- ✅ 从
httpOnly
Cookie 读取 Token - ✅ 转发原始请求方法、Headers、Query、Body
- ✅ 透传 FastAPI 响应(状态码、Headers、Body)
- ✅ 自动添加
Authorization: Bearer <token>
- ✅ 未授权时返回 401
📦 文件结构建议
lib/
└── proxyRouteHandler.ts ← 通用代理逻辑(核心)
app/
└── api/
└── [proxy]/ ← 动态路由(可代理任意 FastAPI 路径)
└── route.ts
1️⃣ 核心代理逻辑:lib/proxyRouteHandler.ts
// lib/proxyRouteHandler.ts
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
/**
* 通用 FastAPI 代理处理器
* @param targetPath - FastAPI 的 API 路径(如 '/recommend', '/users/me')
* @param request - Next.js 的原始请求
* @returns NextResponse
*/
export async function proxyToFastAPI(
targetPath: string,
request: NextRequest
): Promise<NextResponse> {
// 1. 从 httpOnly Cookie 获取 Token
const cookieStore = await cookies();
const token = cookieStore.get('token')?.value;
if (!token) {
return NextResponse.json(
{ error: 'Unauthorized: Missing authentication token' },
{ status: 401 }
);
}
// 2. 构建目标 URL(保留原始查询参数)
const fastapiUrl = new URL(
targetPath,
process.env.FASTAPI_INTERNAL_URL || 'http://localhost:8000'
);
fastapiUrl.search = request.nextUrl.searchParams.toString();
// 3. 准备请求头
const headers = new Headers();
headers.set('Authorization', `Bearer ${token}`);
headers.set('Content-Type', request.headers.get('Content-Type') || 'application/json');
// 可选:透传其他必要头(如 Accept)
if (request.headers.has('Accept')) {
headers.set('Accept', request.headers.get('Accept')!);
}
// 4. 准备请求体(仅非 GET/HEAD 方法)
let body: BodyInit | null = null;
if (!['GET', 'HEAD'].includes(request.method)) {
const contentType = request.headers.get('Content-Type');
if (contentType?.includes('application/json')) {
body = await request.json();
} else if (contentType?.includes('application/x-www-form-urlencoded')) {
body = await request.text();
} else {
// 兜底:尝试读取原始 body
body = await request.text();
}
}
// 5. 转发请求到 FastAPI
try {
const fastapiRes = await fetch(fastapiUrl, {
method: request.method,
headers,
body: body as BodyInit | undefined,
// 关键:禁用 Next.js 缓存,确保实时性
cache: 'no-store',
// 可选:设置超时(需 AbortController)
});
// 6. 透传 FastAPI 的响应(状态码、headers、body)
const responseData = new Uint8Array(await fastapiRes.arrayBuffer());
return new NextResponse(responseData, {
status: fastapiRes.status,
statusText: fastapiRes.statusText,
headers: fastapiRes.headers,
});
} catch (error) {
console.error('Proxy error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
✅ 特点:
- 支持任意 HTTP 方法(GET/POST/PUT/DELETE/PATCH...)
- 自动处理 JSON / form-urlencoded 等常见 Body 类型
- 保留原始查询参数(
?user_id=123
) - 透传 FastAPI 的完整响应(包括自定义 Headers)
2️⃣ 动态代理路由:app/api/[...proxy]/route.ts
// app/api/[...proxy]/route.ts
import { NextRequest } from 'next/server';
import { proxyToFastAPI } from '@/lib/proxyRouteHandler';
/**
* 动态代理路由:将 /api/xxx 映射到 FastAPI 的 /xxx
* 示例:
* GET /api/recommend?user_id=123 → FastAPI /recommend?user_id=123
* POST /api/users → FastAPI /users
*/
export async function GET(request: NextRequest) {
const path = request.nextUrl.pathname.replace(/^\/api/, '');
return proxyToFastAPI(path, request);
}
export async function POST(request: NextRequest) {
const path = request.nextUrl.pathname.replace(/^\/api/, '');
return proxyToFastAPI(path, request);
}
export async function PUT(request: NextRequest) {
const path = request.nextUrl.pathname.replace(/^\/api/, '');
return proxyToFastAPI(path, request);
}
export async function DELETE(request: NextRequest) {
const path = request.nextUrl.pathname.replace(/^\/api/, '');
return proxyToFastAPI(path, request);
}
// 如果需要 PATCH 等方法,继续添加即可
🔥 使用方式:
- 前端调用
fetch('/api/recommend?user_id=123')
- 自动代理到 FastAPI 的
/recommend?user_id=123
- 无需为每个 API 单独写 route.ts!
3️⃣ 环境变量配置(.env.local
)
# FastAPI 内网地址(生产环境应为私有 IP 或服务名)
FASTAPI_INTERNAL_URL=http://localhost:8000
# 如果部署在 Docker Compose,可能是:
# FASTAPI_INTERNAL_URL=http://fastapi:8000
✅ 使用示例
前端调用(任意地方)
// 获取推荐(GET)
const res = await fetch('/api/recommend?user_id=123');
// 更新用户资料(PUT)
await fetch('/api/users/123', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Alice' }),
});
// 登录(POST)— 注意:登录应单独处理(设置 Cookie)
// 但如果你的登录也走这个代理,需在 FastAPI 返回 token 后由 Route Handler 设置 Cookie
// 建议:登录/登出仍用专用 route.ts 以便控制 Cookie
⚠️ 特殊处理:登录/登出(建议单独写)
虽然通用代理能处理登录,但登录需要设置 Cookie,登出需要删除 Cookie,所以建议单独处理:
app/
└── api/
├── auth/
│ ├── login/route.ts ← 专用:处理登录 + 设置 Cookie
│ └── logout/route.ts ← 专用:处理登出 + 删除 Cookie
└── [...proxy]/route.ts ← 通用:代理其他所有 API
// app/api/auth/login/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
export async function POST(request: NextRequest) {
// 1. 调用 FastAPI 登录
const fastapiRes = await fetch('http://localhost:8000/login', {
method: 'POST',
body: await request.text(),
headers: { 'Content-Type': request.headers.get('Content-Type')! },
});
if (!fastapiRes.ok) {
return NextResponse.json({ error: 'Login failed' }, { status: 401 });
}
const { access_token } = await fastapiRes.json();
// 2. 安全设置 httpOnly Cookie
(await cookies()).set({
name: 'token',
value: access_token,
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24, // 24 hours
path: '/',
});
return NextResponse.json({ success: true });
}
// app/api/auth/logout/route.ts
import { NextResponse } from 'next/server';
export async function POST() {
(await cookies()).delete('token');
return NextResponse.json({ success: true });
}
🌟 优势总结
特性 | 说明 |
---|---|
零重复代码 | 一个动态路由代理所有 API |
安全 | Token 通过 httpOnly Cookie 管理 |
灵活 | 支持任意 HTTP 方法和 Body 类型 |
透传 | 完整保留 FastAPI 响应(状态码、Headers) |
生产就绪 | 支持内网部署、环境变量配置 |
这样,你只需:
- 把 FastAPI 部署在内网
- 前端所有 API 调用都走
/api/xxx
- 登录/登出用专用路由处理 Cookie
就能实现安全、简洁、可维护的架构!
在当前架构下,Middleware 和 Route Handler 都检查 Token 确实存在“逻辑重复”,但这种重复不是冗余,而是分层防御(Defense in Depth)的最佳实践。
不过,我们可以优化架构,避免不必要的重复检查。下面详细解释:
🔍 为什么看似“重复”?
层级 | 检查目的 | 是否必要 |
---|---|---|
Middleware | 拦截未登录用户访问页面路由(如/dashboard ) | ✅ 必要(防止页面渲染) |
Route Handler | 拦截未登录用户调用API 路由(如/api/recommend ) | ✅ 必要(防止 API 被直接调用) |
📌 关键区别:
- Middleware 保护的是 页面(Page Routes)
- Route Handler 保护的是 API(API Routes)
🧩 调用场景分析
场景 1:用户访问页面(如 /dashboard
)

✅ 此时 Route Handler 不会执行(因为页面是 Server Component 直连 FastAPI)
场景 2:前端调用 API(如 fetch('/api/recommend')
)

⚠️ 问题来了:
如果 Middleware 的 matcher
包含了 /api
路由,那么:
- Middleware 会先检查一次 Token
- Route Handler 又检查一次 Token
→ 确实重复!
✅ 正确配置:让 Middleware 和 Route Handler 各司其职
方案:Middleware 不拦截 /api
路由,由 Route Handler 单独保护
1. 修改 Middleware 的 matcher
// middleware.ts
export const config = {
matcher: [
// ✅ 只匹配页面路由,排除所有 /api
'/((?!_next/static|_next/image|favicon.ico|api|login).*)',
],
};
🔍 解释:
api
被排除 → Middleware 不会处理/api/xxx
请求/api
请求直接交给对应的 Route Handler 处理
2. Route Handler 保留 Token 检查
// lib/proxyRouteHandler.ts
export async function proxyToFastAPI(...) {
const token = (await cookies()).get('token')?.value;
if (!token) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// ...继续代理
}
🌟 优化后的架构
请求类型 | 处理者 | TOKEN检查次数 |
---|---|---|
页面请求(/dashboard ) | ✅ Middleware | 1 次 |
API 请求(/api/recommend ) | ✅ Route Handler | 1 次 |
登录请求(/api/auth/login ) | ❌ 无检查(公开) | 0 次 |
✅ 无重复,职责清晰!
📝 具体配置步骤
1. Middleware 排除 /api
// middleware.ts
export const config = {
matcher: [
// 匹配所有,但排除:
// - Next.js 静态资源
// - /api 路由(交给 Route Handler 保护)
// - /login(公开页面)
'/((?!_next/static|_next/image|favicon.ico|api|login).*)',
],
};
2. Route Handler 保护所有业务 API
- 通用代理
proxyToFastAPI
检查 Token - 登录/登出等公开 API 不经过代理(单独写,无 Token 检查)
3. 公开 API 明确排除
确保以下路由不在代理范围内:
POST /api/auth/login
POST /api/auth/logout
你的动态代理 [...proxy]
不会匹配 /api/auth/login
(因为存在更具体的 auth/login/route.ts
),所以安全。
🔒 为什么 Route Handler 仍需检查 Token?
即使 Middleware 保护了页面,API 仍可能被直接调用:
- 用户手动输入
https://yoursite.com/api/recommend
- 第三方脚本尝试调用你的 API
- 浏览器插件发起请求
✅ Route Handler 是 API 的最后一道防线,必须独立验证!
✅ 总结:最佳实践
组件 | 职责 | TOKEN检查 |
---|---|---|
Middleware | 保护页面路由 | ✅ 检查(但排除/api ) |
Route Handler | 保护API 路由 | ✅ 检查(所有业务 API) |
公开 API(登录/登出) | 无需保护 | ❌ 不检查 |
这样既避免了重复,又保证了安全。