全栈开发

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(回退)

🏆 方案优势总结

  1. 简单易懂 - 核心逻辑只有两个文件
  2. 零迁移成本 - 现有代码完全兼容
  3. 自动扩展 - 加服务只需改配置,不用改代码
  4. 统一管理 - 所有 API 调用格式一致
  5. 渐进升级 - 从单服务平滑过渡到多服务

这个方案既满足了现在的简单需求,又为未来的扩展留足了空间,完美适合个人开发者! 🚀

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>;
}