全栈开发

Next.js Route Handler 代理模板

下面是一个 通用、安全、可复用的 Next.js Route Handler 代理模板,支持 GETPOSTPUTDELETE 等所有 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)
生产就绪支持内网部署、环境变量配置

这样,你只需:

  1. 把 FastAPI 部署在内网
  2. 前端所有 API 调用都走 /api/xxx
  3. 登录/登出用专用路由处理 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✅ Middleware1 次
API 请求/api/recommend✅ Route Handler1 次
登录请求/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(登录/登出)无需保护❌ 不检查

这样既避免了重复,又保证了安全