文章
Next.js 15 + FastAPI 安全架构模板
📦 项目结构
你的项目/
├── lib/
│ ├── api-config.ts # 服务配置(核心)
│ ├── api-client.ts # 可选工具函数
│ └── server-fetch.ts.ts # 可选工具函数
├── app/
│ ├── api/
│ │ └── [...proxy]/
│ │ └── route.ts # 统一代理路由
│ └── 你的页面...
└── .env.local # 环境配置
📄 文件内容
1. 服务配置文件
// lib/api-config.ts
/**
* FastAPI 服务配置
* 核心思想:默认单服务,预留扩展接口,空配置自动回退到主服务
*/
// 服务配置类型定义
interface ServiceConfig {
url: string; // 服务地址,空字符串表示回退到主服务
prefix: string; // API 路径前缀
}
// 服务配置定义
export const FASTAPI_SERVICES: Record<string, ServiceConfig> = {
// 主服务 - 当前正在使用的核心服务
main: {
url: process.env.FASTAPI_URL || 'http://localhost:8000', // 主服务地址
prefix: '/api/v1' // API 路径前缀
},
// AI 服务 - 预留扩展,用于未来接入 AI 功能
// 如果 FASTAPI_AI_URL 未设置,空字符串会自动回退到主服务
ai: {
url: process.env.FASTAPI_AI_URL || '', // 空 = 回退到主服务
prefix: '/api/v1'
},
// 统计分析服务 - 预留扩展,用于未来接入数据分析
analytics: {
url: process.env.FASTAPI_ANALYTICS_URL || '', // 空 = 回退到主服务
prefix: '/api/v1'
}
};
/**
* 根据请求路径识别目标服务
* 实现智能路由:特定前缀走特定服务,其他走主服务
*
* @param path 请求路径,如 'users/profile' 或 'ai/chat'
* @returns 目标服务名称和清理后的路径
*
* @example
* getTargetService('users/profile') // { service: 'main', cleanPath: 'users/profile' }
* getTargetService('ai/recommend') // { service: 'ai', cleanPath: 'recommend' }
*/
export function getTargetService(path: string): { service: string; cleanPath: string } {
// AI 相关请求路由到 AI 服务
if (path.startsWith('ai/')) {
return {
service: 'ai',
cleanPath: path.replace('ai/', '') // 移除 'ai/' 前缀
};
}
// 统计分析请求路由到分析服务
if (path.startsWith('analytics/')) {
return {
service: 'analytics',
cleanPath: path.replace('analytics/', '') // 移除 'analytics/' 前缀
};
}
// 默认路由到主服务(包括未识别的路径)
return {
service: 'main',
cleanPath: path // 保持原路径
};
}
/**
* 获取服务的实际 URL
* 核心特性:如果服务 URL 为空,自动回退到主服务
*
* @param service 服务名称
* @returns 服务的基础 URL
*
* @example
* getServiceUrl('ai') // 如果 FASTAPI_AI_URL 未设置,返回主服务地址
*/
export function getServiceUrl(service: string): string {
const config = FASTAPI_SERVICES[service];
if (!config) {
// 服务不存在,回退到主服务
return FASTAPI_SERVICES.main.url;
}
// 如果服务 URL 为空,回退到主服务
return config.url || FASTAPI_SERVICES.main.url;
}
2. 统一代理路由
// app/api/[...proxy]/route.ts
import { NextRequest } from 'next/server';
import { getTargetService, getServiceUrl, FASTAPI_SERVICES } from '@/lib/api-config';
/**
* 统一代理路由
* 功能:接收所有 /api/proxy/* 请求,转发到对应的 FastAPI 服务
* 优势:隐藏后端地址,统一处理认证,支持多服务扩展
*/
// 支持的 HTTP 方法 - 根据需要添加
export async function GET(
request: NextRequest,
{ params }: { params: { proxy: string[] } }
) {
return handleProxyRequest(request, params);
}
export async function POST(
request: NextRequest,
{ params }: { params: { proxy: string[] } }
) {
return handleProxyRequest(request, params);
}
export async function PUT(
request: NextRequest,
{ params }: { params: { proxy: string[] } }
) {
return handleProxyRequest(request, params);
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { proxy: string[] } }
) {
return handleProxyRequest(request, params);
}
/**
* 代理请求处理核心函数
* 处理流程:
* 1. 解析请求路径 → 2. 识别目标服务 → 3. 构建目标URL → 4. 转发请求 → 5. 返回响应
*/
async function handleProxyRequest(
request: NextRequest,
params: { proxy: string[] }
) {
// 1. 解析请求路径和查询参数
const fullPath = params.proxy.join('/'); // 将数组转为路径字符串
const searchParams = request.nextUrl.searchParams.toString();
console.log(`[代理] ${request.method} ${fullPath}`);
// 2. 智能路由:识别目标服务和清理路径
const { service, cleanPath } = getTargetService(fullPath);
// 3. 获取目标服务地址(支持自动回退)
const rawConfig = FASTAPI_SERVICES[service];
const fallbackToMain = !rawConfig || !rawConfig.url;
const effectiveConfig = fallbackToMain ? FASTAPI_SERVICES.main : rawConfig;
const baseUrl = effectiveConfig.url;
const prefix = effectiveConfig.prefix;
// 4. 构建目标 URL
const url = `${baseUrl}${serviceConfig.prefix}/${cleanPath}${
searchParams ? `?${searchParams}` : '' // 保留查询参数
}`;
// 5. 从 cookie 获取认证 token
const token = request.cookies.get('auth-token')?.value;
try {
// 6. 转发请求到目标服务
const response = await fetch(url, {
method: request.method,
headers: {
// 自动添加认证头(如果有 token)
...(token && { Authorization: `Bearer ${token}` }),
'Content-Type': 'application/json',
},
// GET 和 DELETE 请求没有 body
body: request.method === 'GET' || request.method === 'DELETE'
? undefined
: await request.text(), // 直接传递请求体
});
// 7. 直接传递目标服务的响应(保持状态码和内容类型)
return new Response(response.body, {
status: response.status,
headers: {
'Content-Type': response.headers.get('Content-Type') || 'application/json',
},
});
} catch (error) {
// 8. 统一错误处理
console.error(`[代理错误] ${request.method} ${fullPath}:`, error);
return Response.json(
{
error: '服务暂时不可用',
message: '后端服务响应超时,请稍后重试',
path: fullPath
},
{ status: 503 } // 服务不可用状态码
);
}
}
3. 可选:简单 API 客户端
// lib/api-client.ts
/**
* 可选工具函数 - 提供更友好的 API 调用方式
* 如果你喜欢直接使用 fetch,可以跳过这个文件
*/
/**
* 统一的 API 调用函数
* 自动处理服务端和客户端的 URL 差异
*
* @param endpoint API 端点,如 'users/profile' 或 'ai/chat'
* @param options fetch 选项
* @returns Promise 解析响应数据
*/
async function apiFetch<T = any>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
// 智能确定基础 URL:
// - 服务端:需要绝对路径(http://localhost:3000/api/proxy/...)
// - 客户端:使用相对路径(/api/proxy/...)
const baseUrl = typeof window === 'undefined'
? process.env.NEXTAUTH_URL || 'http://localhost:3000' // 服务端
: ''; // 客户端
// 构建完整的代理 URL
const cleanEndpoint = endpoint.replace(/^\//, ''); // 移除开头的斜杠
const url = `${baseUrl}/api/proxy/${cleanEndpoint}`;
console.log(`[API调用] ${endpoint}`);
try {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
// 客户端请求自动携带 cookies(用于认证)
...(typeof window !== 'undefined' && { credentials: 'include' }),
});
if (!response.ok) {
// 统一错误处理
const errorData = await response.json().catch(() => ({
error: `请求失败,状态码: ${response.status}`
}));
throw new Error(errorData.message || errorData.error || 'API 请求失败');
}
return await response.json();
} catch (error) {
console.error(`[API错误] ${endpoint}:`, error);
throw error;
}
}
/**
* 按功能组织的 API 方法
* 提供更语义化的调用方式
*/
export const api = {
// ==================== 用户相关 ====================
users: {
/** 获取当前用户资料 */
getProfile: () => apiFetch<{ id: string; name: string; email: string }>('users/profile'),
/** 更新用户资料 */
updateProfile: (data: { name?: string; email?: string }) =>
apiFetch('users/profile', {
method: 'PUT',
body: JSON.stringify(data)
}),
},
// ==================== 商品相关 ====================
products: {
/** 获取商品列表 */
getList: (category?: string) =>
apiFetch(`products${category ? `?category=${category}` : ''}`),
/** 获取商品详情 */
getDetail: (id: string) => apiFetch(`products/${id}`),
},
// ==================== AI 相关 ====================
ai: {
/** 获取推荐内容 */
getRecommendations: (userId: string) =>
apiFetch('ai/recommend', {
method: 'POST',
body: JSON.stringify({ user_id: userId })
}),
/** AI 对话 */
chat: (message: string) =>
apiFetch('ai/chat', {
method: 'POST',
body: JSON.stringify({ message })
}),
},
// ==================== 统计分析 ====================
analytics: {
/** 获取统计数据 */
getStats: (period: 'week' | 'month') =>
apiFetch(`analytics/stats?period=${period}`),
}
};
/**
* 原始 fetch 的快捷方式(如果你更喜欢直接用 fetch)
*/
export const fetchAPI = apiFetch;
4.创建 lib/server-fetch.ts
// lib/server-fetch.ts
import { cookies } from 'next/headers';
import { NextRequest } from 'next/server';
export async function fetchWithAuth(
path: string,
init?: RequestInit
) {
// 1. 获取 token:用 next/headers(Server Component/Action)
const token = (await cookies()).get('auth-token')?.value;
// 2. 构建完整 FastAPI URL
const FASTAPI_URL = process.env.FASTAPI_URL || 'http://localhost:8000';
const url = `${FASTAPI_URL}/api/v1/${path.replace(/^\/+/, '')}`; // 去掉开头的 /
// 3. 发起请求
const res = await fetch(url, {
...init,
headers: {
...(token && { Authorization: `Bearer ${token}` }),
'Content-Type': 'application/json',
...init?.headers,
},
});
// 4. 统一错误处理(可选但推荐)
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
console.error(`[Server Fetch Error] ${path}`, {
status: res.status,
error: errorData,
});
// 可选择抛出或返回错误结构
throw new Error(`API error: ${res.status}`);
}
return res.json();
}
5. 环境配置
# .env.local
# FastAPI 服务配置
# 主服务地址(必须)
FASTAPI_URL=http://localhost:8000
# AI 服务地址(可选 - 注释掉则自动回退到主服务)
# FASTAPI_AI_URL=http://localhost:8001
# 统计分析服务地址(可选 - 注释掉则自动回退到主服务)
# FASTAPI_ANALYTICS_URL=http://localhost:8002
# Next.js 应用地址(服务端调用时使用)
NEXTAUTH_URL=http://localhost:3000
💡 完整使用示例
方式1:直接使用 fetch(最简单)
// app/profile/page.tsx - Server Component
export default async function ProfilePage() {
// 直接调用,不需要额外工具
const user = await fetch('http://localhost:3000/api/proxy/users/profile')
.then(r => r.json());
const products = await fetch('http://localhost:3000/api/proxy/products')
.then(r => r.json());
return (
<div>
<h1>{user.name}</h1>
<div>
{products.map((product: any) => (
<div key={product.id}>{product.name}</div>
))}
</div>
</div>
);
}
// components/ProductList.tsx - Client Component
'use client'
import { useEffect, useState } from 'react';
export function ProductList() {
const [products, setProducts] = useState([]);
useEffect(() => {
// 客户端使用相对路径
fetch('/api/proxy/products')
.then(r => r.json())
.then(setProducts);
}, []);
return <div>{products.map((p: any) => <div key={p.id}>{p.name}</div>)}</div>;
}
方式2:使用 API 客户端(更优雅)
// 使用 api-client.ts 提供的工具
import { api } from '@/lib/api-client';
// Server Component
export default async function ProfilePage() {
const user = await api.users.getProfile();
const products = await api.products.getList();
const recommendations = await api.ai.getRecommendations(user.id);
return (
<div>
<h1>{user.name}</h1>
<p>推荐内容: {recommendations.length} 条</p>
</div>
);
}
// Client Component
'use client'
import { api } from '@/lib/api-client';
import { useEffect, useState } from 'react';
export function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
api.users.getProfile().then(setUser);
}, []);
return user ? <div>{user.name}</div> : <div>加载中...</div>;
}
方式3:Server Actions
// app/actions.ts
'use server'
import { api } from '@/lib/api-client';
export async function updateUserProfile(data: { name: string; email: string }) {
try {
const user = await api.users.updateProfile(data);
return { success: true, user };
} catch (error) {
return { success: false, error: (error as Error).message };
}
}
🎯 路由映射示例
当前阶段(单服务)
浏览器请求 → 实际服务
/api/proxy/users/profile → http://localhost:8000/api/v1/users/profile
/api/proxy/products → http://localhost:8000/api/v1/products
/api/proxy/ai/recommend → http://localhost:8000/api/v1/ai/recommend(回退)
/api/proxy/analytics/stats → http://localhost:8000/api/v1/analytics/stats(回退)
未来阶段(接入 AI 服务)
# 在 .env 中添加:
FASTAPI_AI_URL=http://localhost:8001
浏览器请求 → 实际服务
/api/proxy/users/profile → http://localhost:8000/api/v1/users/profile
/api/proxy/products → http://localhost:8000/api/v1/products
/api/proxy/ai/recommend → http://localhost:8001/api/v1/recommend(独立服务)
/api/proxy/analytics/stats → http://localhost:8000/api/v1/analytics/stats(回退)
🏆 方案优势总结
- 简单易懂 - 核心逻辑只有两个文件
- 零迁移成本 - 现有代码完全兼容
- 自动扩展 - 加服务只需改配置,不用改代码
- 统一管理 - 所有 API 调用格式一致
- 渐进升级 - 从单服务平滑过渡到多服务
这个方案既满足了现在的简单需求,又为未来的扩展留足了空间,完美适合个人开发者! 🚀
fetchWithAuth示例:
1、在 Server Component 中使用
// app/dashboard/page.tsx
import { fetchWithAuth } from '@/lib/server-fetch';
export default async function Dashboard() {
// 自动读取当前请求的 Cookie 中的 token
const user = await fetchWithAuth('/users/me');
const recommendations = await fetchWithAuth('/ai/recommend', {
method: 'POST',
body: JSON.stringify({ user_id: user.id }),
});
return <div>...</div>;
}
2、在 Server Action 中使用
// app/dashboard/page.tsx
import { fetchWithAuth } from '@/lib/server-fetch';
export default async function Dashboard() {
// 自动读取当前请求的 Cookie 中的 token
const user = await fetchWithAuth('/users/me');
const recommendations = await fetchWithAuth('/ai/recommend', {
method: 'POST',
body: JSON.stringify({ user_id: user.id }),
});
return <div>...</div>;
}