恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
FastAPI + React 前后端分离全栈实战:从API设计到部署
首页
资讯中心
/
FastAPI + React 前后端分离全栈实战:从API设计到部署
FastAPI + React 前后端分离全栈实战:从API设计到部署
发布时间:2026/10/3 15:37:30
1. 为什么是 FastAPI React这个组合到底解决了什么问题先说一个我自己的真实场景。去年我要给团队内部做一个数据看板工具需求不复杂前端展示几个图表后端对接数据库返回聚合结果用户登录后各看各的报表。按理说随便找个模板引擎渲染一下也够用但后来需求越加越多要支持不同角色的权限控制、要对接第三方登录、要给外部系统开放接口原本渲染页面的那点逻辑完全不够用了。最后我决定彻底切到前后端分离架构后端选了 FastAPI前端选了 React。跑通之后我才真正体会到这个组合解决的不是某个页面怎么写的问题而是整个团队协作方式和项目维护成本的问题。前后端分离的核心思想简单说就是后端只用 JSON 说话前端只管页面渲染。后端不再关心用户浏览器里长什么样它只负责把数据算好、返回出去前端也不再拼模板字符串它专注做交互和展示。这样一来后端接口可以被 Web 页面、小程序、App、自动化脚本任意调用前端也可以用最快的框架去构建交互体验两边只要把 API 约定好就能并行开发互不阻塞。FastAPI 是这个分工里后端侧非常顺手的一个选择React 则是前端侧生态成熟度最高的选项之一两者拼在一起就是现代全栈的一种标准打开方式。这套组合到底适合谁我觉得有三类人最值得看一是已经会 Python 但想补前端能力的开发者FastAPI 几乎零学习门槛你只需要知道 Python 语法就能写接口二是团队里本来用 Flask/Django 写模板页面的朋友想升级到前后端分离但不知道从哪里下手三是正在做课程设计、毕设或者个人作品集的同学前后端分离项目的完整度在面试和展示中都更有说服力。如果你属于其中之一这篇文章应该能帮你省掉不少弯路。2. 开工前的技术选型与环境准备别在第一步翻车2.1 为什么是 FastAPI 而不是 Flask 或 Django很多人问我用现成的 Flask 或者 Django 不好吗为什么要换 FastAPI我的回答是如果项目很小、逻辑简单Flask 完全没问题如果项目有复杂的 Admin 后台和 ORM 体系Django 也很成熟。但 FastAPI 的定位刚好卡在中间——它既有 Flask 的轻量灵活又具备 Django 没有的现代特性。对比维度FastAPIFlaskDjango性能基于 Starlette 和 Uvicorn异步原生同步为主高并发场景偏弱同步为主重框架开销大数据校验Pydantic 自动校验定义即校验手写校验逻辑依赖 Django Form/DRF SerializerAPI 文档自动生成 Swagger 和 ReDoc需额外接入 flasgger需额外配置 drf-yasg类型提示原生支持写接口像写函数签名基本不涉及部分支持学习成本低通读文档一天就能上手极低高自带体系多我最看重的是 FastAPI 的类型提示驱动的开发方式。你定义一个 Pydantic 模型就等于同时定义了请求体结构、响应体结构、数据校验规则和 API 文档一份代码四份用途。这在前后端分离的项目里非常关键因为前后端需要靠接口文档对齐FastAPI 自动生成的 Swagger 页面直接扔给前端同事他连 Postman 都不用配就能看到每个字段的类型、是否必填、示例值。我在若干项目里试验下来这种接口即文档的模式省掉了大量口头沟通和对字段的扯皮。2.2 用 uv 管理 Python 虚拟环境绕开安装怪坑环境准备这件事看着不起眼却是我见过翻车最多的地方。很多人一上来就在全局环境里pip install fastapi然后过几天被各种包版本冲突折腾到崩溃。我的习惯是用虚拟环境隔离每个项目的依赖工具方面现在强烈推荐 uv它的速度比 pip 快一个量级还自带 Python 版本管理。用 uv 创建一个 FastAPI 项目的虚拟环境几步就完成了# 安装 uvmacOS / Linux curl -LsSf https://astral.sh/uv/install.sh | sh # 初始化项目目录并创建虚拟环境 mkdir my-project cd my-project uv venv .venv # 激活虚拟环境Windows 用 .venv\Scripts\activate source .venv/bin/activate # 安装 FastAPI 和 Uvicorn uv pip install fastapi uvicorn[standard]有个来自真实使用场景的细节很多人安装 fastapi 失败或者装完启动报错十有八九是因为 Python 版本不对。FastAPI 要求 Python 3.8 及以上但我在 PyCharm 里就遇到过选了老版本解释器导致 Pydantic 装不上、代码里一堆红波浪线的情况。解决方式很简单在 PyCharm 的 Settings - Project - Python Interpreter 里确认解释器路径指向刚才用 uv 创建的.venv而不是系统自带的 Python。还要留意一下这个虚拟环境里 pip 指向的是不是项目目录如果指错了装的东西全进全局环境项目里照样 import 不到。2.3 React 前端脚手架用 Vite 而不是 CRA前端侧我踩过一个比较疼的坑。以前建 React 项目我习惯用 create-react-app但这个脚手架越来越慢依赖安装动辄几分钟构建也要等半天。后来新项目我全部切到 Vite同样是 ReactVite 的冷启动几乎是秒开热更新也快一个档次。创建方式很简单npm create vitelatest frontend -- --template react-ts cd frontend npm install npm run dev这里建议直接选react-ts模板。虽然 TS 会多写一点类型代码但前后端分离项目里前端接口层的 TypeScript 类型可以直接对照后端的 Pydantic 模型手动定义能提前暴露很多字段不匹配的问题而不是等运行到页面上才报错。好的全栈项目前后端之间应该有类型默契TypeScript 就是前端侧守住这条线的工具。2.4 项目目录结构别把前后端揉成一坨目录组织直接决定项目维护体验。我踩过一次把前后端代码放在同一个目录下、dependencies 互相引用的坑之后现在统一采用这样的结构my-project/ ├── backend/ │ ├── app/ │ │ ├── __init__.py │ │ ├── main.py # FastAPI 入口 │ │ ├── config.py # 配置读取 │ │ ├── models.py # Pydantic / ORM 模型 │ │ ├── routers/ # 按业务模块拆分的路由 │ │ ├── schemas.py # 请求响应数据结构 │ │ └── database.py # 数据库连接 │ ├── .env │ └── requirements.txt ├── frontend/ │ ├── src/ │ │ ├── api/ # 接口请求封装 │ │ ├── components/ # 通用组件 │ │ ├── pages/ # 页面级组件 │ │ ├── stores/ # 全局状态 │ │ └── App.tsx │ ├── package.json │ └── vite.config.ts └── README.md前后端分属两个独立目录各自拥有独立的依赖和启动方式只在接口层面做约定。这样做的最大好处是后端可以单独测试、单独部署前端也可以单独跑起来用 mock 数据开发谁都不需要等谁。3. 后端先行用 FastAPI 搭建可扩展的 API 骨架3.1 配置文件别再硬编码连接串了我在真实项目里见过太多人把数据库地址、密钥、第三方接口的 token 直接写在代码里一旦换环境就要改代码重新部署非常痛苦。FastAPI 项目里最标准的做法是用pydantic-settings读取配置文件。# backend/app/config.py from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config SettingsConfigDict(env_file.env, env_file_encodingutf-8) app_name: str FastAPI React Demo debug: bool False database_url: str sqlite:///./test.db secret_key: str change-me cors_origins: list[str] [http://localhost:5173] settings Settings()然后在.env文件里写实际值APP_NAMEMy Project DEBUGtrue DATABASE_URLpostgresql://user:passwordlocalhost:5432/mydb SECRET_KEYyour-secret-key CORS_ORIGINS[http://localhost:5173,http://127.0.0.1:5173]在 main.py 里直接from app.config import settings就能全局使用代码里不出现任何环境相关的硬编码。这个习惯越早养成越好后面部署到 Linux 服务器时只需要在服务器上配一份.env代码不用动任何一行。我第一次在服务器上部署时因为没做配置分离被迫临时改了代码里的数据库地址重新构建之后才发现SECRET_KEY也写死在代码里简直想抽自己。3.2 定义数据模型一份代码四份用途接下来定义一个简单的业务模型用待办事项Todo项目做例子。FastAPI 配合 Pydantic接口的定义非常直观# backend/app/schemas.py from pydantic import BaseModel class TodoCreate(BaseModel): title: str description: str | None None class Todo(TodoCreate): id: int completed: bool False class Config: from_attributes True# backend/app/main.py from fastapi import FastAPI from app.config import settings from app.schemas import Todo, TodoCreate app FastAPI(titlesettings.app_name) # 用一个内存列表模拟数据库实际开发换成 SQLAlchemy 连真实数据库 fake_db [] current_id 0 app.get(/api/todos, response_modellist[Todo]) def list_todos(): return fake_db app.post(/api/todos, response_modelTodo, status_code201) def create_todo(payload: TodoCreate): global current_id current_id 1 todo Todo(idcurrent_id, **payload.model_dump()) fake_db.append(todo) return todo启动服务uvicorn app.main:app --reload --port 8000启动之后打开http://localhost:8000/docs你会看到 Swagger 页面已经把两个接口列出来了还能直接在页面上测试请求。这就是前面说的自动生成 API 文档的威力前端同事只要能打开这个地址就能搞清楚接口怎么调。我在实际项目里习惯把路由按业务模块拆分到routers/目录下比如routers/todos.py、routers/auth.py然后通过app.include_router()挂载。项目一旦超过两三个模块全部写在 main.py 里就没法维护了拆分的时机不要等从一开始就按模块建文件后面扩展起来会舒服很多。3.3 数据库会话与依赖注入连接别随手关真实项目肯定不能用内存列表至少要接一个 SQLite 或 PostgreSQL。FastAPI 搭配 SQLAlchemy 是常见组合。这里我不展开完整 ORM 配置重点说一个非常容易被忽略的设计数据库会话的创建和关闭应该通过 FastAPI 的依赖注入来处理。# backend/app/database.py from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, Session DATABASE_URL sqlite:///./todos.db engine create_engine(DATABASE_URL, connect_args{check_same_thread: False}) SessionLocal sessionmaker(autocommitFalse, autoflushFalse, bindengine) def get_db(): db SessionLocal() try: yield db finally: db.close()然后在路由函数里这样用router.get(/api/todos, response_modellist[Todo]) def list_todos(db: Session Depends(get_db)): return db.query(TodoModel).all()这段代码的精妙之处在于你不需要在每个接口里手动db.close()FastAPI 的依赖注入系统会在请求结束后自动执行get_db中finally块里的清理逻辑。这样做不仅避免连接泄漏也让代码非常干净。我第一次写时不懂这个模式每个函数里都复制粘贴一遍获取会话和关闭会话的代码改起来头大后来统一改成依赖注入整体代码量少了一半。4. 前后端联调的拦路虎CORS 跨域问题从报错到解决4.1 跨域是怎么发生的为什么浏览器要拦你前后端分离后遇到的第一个拦路虎几乎百分之百是 CORS。你在前端npm run dev启动在http://localhost:5173后端跑在http://localhost:8000浏览器打开页面后JavaScript 去请求后端的接口控制台就会冒出一片红报错信息大概长这样Access to fetch at http://localhost:8000/api/todos from origin http://localhost:5173 has been blocked by CORS policy这个报错其实是浏览器的安全机制在起作用它的逻辑是不同源协议、域名、端口任一不同之间的请求默认是不被信任的。浏览器本质上是帮用户把关防止某个网站偷偷去请求其他网站的数据。但前后端分离的开发模式下前端和后端天然就是不同源的就会撞上这堵墙。4.2 CORSMiddleware 的基本配置FastAPI 解决跨域有专门的中间件代码量非常少# backend/app/main.py from fastapi.middleware.cors import CORSMiddleware from app.config import settings app FastAPI(titlesettings.app_name) app.add_middleware( CORSMiddleware, allow_originssettings.cors_origins, allow_credentialsTrue, allow_methods[*], allow_headers[*], )几个参数说明一下allow_origins允许哪些源的请求。开发环境写[http://localhost:5173]生产环境要改成实际部署的域名。allow_methods允许哪些 HTTP 方法。[*]表示全部一般就这么设。allow_headers允许哪些请求头。[*]表示全部如果前端要传Authorization头通常设[*]即可。allow_credentials是否允许携带 Cookie。如果前端需要带 Cookie 请求必须设为True。4.3 我在 CORS 上踩过的三个真实坑坑一allow_origins写错了还浑然不知。有一次前端用http://127.0.0.1:5173访问但我在后端配置里只写了http://localhost:5173结果一路排查了半天才想起来 Origin 里有127.0.0.1和localhost的区别。建议干脆两个都写上或者在生产环境用一个域名就只留一个。坑二allow_credentialsTrue时allow_origins不能是*。浏览器对携带凭证的跨域请求要求 Origin 必须精确匹配不能使用通配符。很多教程里写allow_origins[*]你照着配了结果前端加了credentials: include之后依然报错。所以生产环境要老老实实写具体的 Origin 列表。坑三预检请求OPTIONS被前端当成报错。当你的请求使用了非简单请求头比如Authorization或自定义方法时浏览器会先发一个OPTIONS请求去探测服务端允不允许。有段时间我的前端同事看到 Network 面板里有OPTIONS /api/todos返回 405就以为接口挂了其实只要 CORSMiddleware 配置正确这个预检请求是会被正常处理的前端真正关心的还是后面那个实际请求。后来我在文档里明确告诉同事看到OPTIONS成功返回 200说明预检通过不用管它。解决 CORS 还有一个偷懒方案在 Vite 的配置文件里设置代理让前端开发服务器把/api开头的请求转发到后端这样浏览器看来请求始终是同源的CORS 问题直接消失// vite.config.ts export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8000, changeOrigin: true, }, }, }, });如果你用了代理后端其实可以不配 CORSMiddleware。但这个方案只对开发环境有效生产环境还是要靠后端正确配置跨域或者用同一个域名部署前后端。我在团队里的习惯是开发环境用 Vite 代理避开跨域生产环境用 Nginx 做同域部署后端 CORS 作为兜底配置保留三层下来基本不会出问题。5. 前端对接Axios 封装、状态管理与接口层的设计5.1 封装请求层别在每个组件里裸写 fetch前端对接后端最忌讳的就是每个页面组件里直接fetch请求随便写写 URL不看错误不处理加载状态。项目大了之后接口地址散落各处改一个 baseURL 要全局搜索非常崩溃。我的做法是封装一个统一的请求模块以 Axios 为例// frontend/src/api/client.ts import axios from axios; const client axios.create({ baseURL: /api, timeout: 10000, }); // 请求拦截器自动带上 token client.interceptors.request.use((config) { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); // 响应拦截器统一处理错误 client.interceptors.response.use( (response) response.data, (error) { if (error.response?.status 401) { // token 过期跳转登录页 window.location.href /login; } return Promise.reject(error); } ); export default client;这样每个业务模块的请求函数就非常干净// frontend/src/api/todos.ts import client from ./client; import type { Todo, TodoCreate } from ../types/todo; export const getTodos () client.getTodo[](/todos); export const createTodo (data: TodoCreate) client.postTodo(/todos, data);baseURL: /api配合 Vite 代理就是我在上一节提到的开发方案。代码里所有请求都写在api/目录下前端同事看这个目录就能知道项目一共调了哪些接口后端接口变动时也能快速定位需要改动的位置。5.2 用 Zustand 管理前端全局状态React 项目里全局状态管理很多人第一反应是 Redux。但我的体验是Redux 的样板代码太多对一个轻量全栈项目纯属负担。我更推荐 Zustand它的 API 设计极其简洁用起来几乎没有学习成本。// frontend/src/stores/useAuthStore.ts import { create } from zustand; interface AuthState { user: { username: string } | null; token: string | null; login: (token: string, user: { username: string }) void; logout: () void; } export const useAuthStore createAuthState((set) ({ user: null, token: null, login: (token, user) set({ token, user }), logout: () set({ token: null, user: null }), }));在组件里使用import { useAuthStore } from ../stores/useAuthStore; function Header() { const user useAuthStore((state) state.user); const logout useAuthStore((state) state.logout); return ( div {user ? ( span{user.username} button onClick{logout}退出/button/span ) : ( span未登录/span )} /div ); }我用 Zustand 最大的感受是它没有把 React 的状态管理复杂化。不需要 Provider 包裹不需要高阶组件一个create函数就完事特别适合中小型全栈项目。5.3 服务端请求状态用 TanStack Query 管起来除了客户端自己的状态前后端分离项目里还有一类服务端状态——比如列表数据、详情数据这些是从后端读取的。早期我习惯把这些数据也放进全局 store 里管理后来发现这带来无尽的同步问题另一个组件改了数据列表不知道要不要更新缓存怎么失效等等。TanStack Query以前叫 React Query专门解决这个问题。它帮你处理请求的加载态、错误态、缓存和自动重新拉取写起来也很直接// frontend/src/features/TodoList.tsx import { useQuery, useMutation, useQueryClient } from tanstack/react-query; import { getTodos, createTodo } from ../api/todos; function TodoList() { const queryClient useQueryClient(); const { data: todos, isLoading } useQuery({ queryKey: [todos], queryFn: getTodos, }); const createMutation useMutation({ mutationFn: createTodo, onSuccess: () { queryClient.invalidateQueries({ queryKey: [todos] }); }, }); if (isLoading) return div加载中.../div; return ( div {todos?.map((todo) div key{todo.id}{todo.title}/div)} button onClick{() createMutation.mutate({ title: 新任务 })} 添加 /button /div ); }看到这段代码的妙处了吗invalidateQueries会在创建成功后自动让getTodos重新拉取不需要手动刷新页面或管理数据更新逻辑。在真实项目里这种模式省掉了大量改完数据就刷新列表的低级代码。6. 联调、调试与部署从本地跑通到线上可用6.1 本地联调的标准流程前后端联调听起来很复杂其实只要双方都启动在本地按固定流程走就行。我的习惯是后端启动uvicorn app.main:app --reload --port 8000确认 Swagger 可访问。前端启动npm run dev确认页面能打开。先用 Vite 代理配置好/api转发浏览器里打开页面按 F12 看 Network 面板逐个接口验证。后端加日志或断点前端在 DevTools 的 Console 和 Network 里对照请求参数和响应体。遇到字段对不上直接在 Swagger 页面看后端实际返回遇到类型错误用 TypeScript 的报错信息对照 Pydantic 模型。这里有一个非常实用的调试技巧看接口问题优先级永远先看 Network 面板而不是直接去看代码。Network 里能看到请求是否发出、请求头是什么、响应体是什么、状态码是多少信息比 IDE 里 print 出来的还全。有几次同事说接口返回有问题我一打开 Network 就发现请求根本没发出去是前端拦截器在 header 里莫名其妙加了个空 token压根跟后端无关。这个经验让我养成了一个习惯先确认去和后端的数据通路是好的再谈代码逻辑。6.2 构建前端产物并用 Nginx 部署本地联调通过之后就到了上线环节。前后端分离项目部署的方式很多我比较推荐用 Nginx 同时托管前端静态文件和反代后端接口这样最终用户只需要访问一个域名浏览器不会遇到跨域问题。先构建前端cd frontend npm run build构建完成后会在frontend/dist目录产出静态文件HTML、JS、CSS。然后配置 Nginxserver { listen 80; server_name your-domain.com; # 前端静态文件 root /var/www/my-project/frontend/dist; index index.html; # 所有 /api 开头的请求转发到 FastAPI location /api/ { proxy_pass http://127.0.0.1:8000/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 前端路由 history 模式回退 location / { try_files $uri $uri/ /index.html; } }这里有几个坑我反复踩过。第一个是try_files $uri $uri/ /index.html这一行如果前端使用了 React Router 的 BrowserRouter即 history 模式必须加上这一行否则用户直接访问https://your-domain.com/dashboard这个路径时Nginx 会返回 404因为服务器上确实没有dashboard这个物理文件。加了这行之后所有没有对应物理文件的路径都会回退到index.html由 React 路由接管。第二个坑是proxy_pass后面的路径拼接。location /api/配合proxy_pass http://127.0.0.1:8000/api/会把请求原样转发如果proxy_pass写成了http://127.0.0.1:8000后面没有/api/Nginx 会把整个/api/xxx路径拼到后端地址后面CPU 想半天才知道转发错了。我自己因为这个细节排查过一个晚上最后 curl 后端接口发现直接访问 8000 端口是通的走 Nginx 就 404才意识到是路径配置的问题。6.3 后端进程管理别只用裸的 uvicorn生产环境用uvicorn app.main:app --host 0.0.0.0 --port 8000直接跑虽然能工作但终端一关进程就没了服务器重启也不会自动拉起。我的做法是配合 systemd 把 FastAPI 服务设为系统服务。# /etc/systemd/system/my-fastapi.service [Unit] DescriptionFastAPI Application Afternetwork.target [Service] Userwww-data WorkingDirectory/var/www/my-project/backend ExecStart/var/www/my-project/backend/.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 Restartalways RestartSec3 EnvironmentFile/var/www/my-project/backend/.env [Install] WantedBymulti-user.target然后执行sudo systemctl daemon-reload sudo systemctl enable my-fastapi.service sudo systemctl start my-fastapi.service这样进程崩溃了会自动重启服务器重启了服务会自己拉起来。EnvironmentFile指定了环境变量文件保证.env里的配置能被正确加载。注意ExecStart里我用了虚拟环境中的 uvicorn 绝对路径这样做是为了避开系统里装的其他 Python 包的影响。还有一个小建议生产环境可以给 uvicorn 加几个 workers 提升并发处理能力比如--workers 4。但要注意如果你的代码里有依赖内存存储的数据比如我那节示例里的fake_db多 worker 会导致数据不共享每个 worker 各存一份。生产环境千万别用内存存储数据库一定要独立出来这是多进程部署的基本前提。6.4 上线后容易忽略的几个细节部署完成后我吃过几次亏之后总结了一套上线自检清单每一条都是用时间换来的教训环境变量要单独管理。.env文件不要提交到 Git 仓库尤其是包含密钥、数据库密码、第三方 token 的。在服务器上单独创建并给予正确的文件权限。SECRET_KEY 要换掉默认值。很多人上线后忘了改默认密钥被人拿到 JWT 签名的 key 之后可以直接伪造 token。我的习惯是一部署就生成一个新的随机串。看日志是最快的排障入口。前端报错先看 Network后端报错先看 journalctl。systemd 服务的日志可以通过journalctl -u my-fastapi.service -f实时查看。数据库备份要提前想好。PostgreSQL 可以用pg_dump做定时备份SQLite 直接复制文件也行但前提是你记得有这个事。我在一次误删数据后就把备份脚本写进了 crontab隔天跑一次。HTTPS 不能忽略。如果部署在公网建议尽早用 certbot 配好证书浏览器地址栏不带锁的网站在现在的用户眼里基本等于不安全三个字。我在实际部署中还发现一个比较隐蔽的问题前端项目里如果用了路由的basename或者接口的baseURL一旦部署路径不是根路径比如部署在https://domain.com/my-app/就要额外考虑静态资源引用地址和 API 路径前缀的问题。最好的办法是部署前就在前端代码里用环境变量控制路径而不是硬编码/。另外当部署的服务器上想再塞进第二个前端项目时Nginx 配置就需要在server块里多写几个location或者干脆用不同的 server_name 区分。我早年在一个服务器上同时部署了三个 Web 项目全是靠 server_name 分开的Nginx 配置里互相不干扰管理起来也清楚。多个项目共用一个服务器时建议每个项目一个 server 块端口都用 80域名或子域名区分别把它们串在同一个 location 规则里否则改一处影响全局会非常痛苦。7. 写在最后一个小技巧和一句真心话所有东西跑通之后我发现一个让全栈开发体验提升不少的小习惯把后端的 Swagger 地址和前端的本地开发地址固定在 README 里每次新同事加入项目或者隔一段时间重新打开这个项目照着 README 两分钟内就能把所有服务跑起来不用到处问这个项目怎么启动。别小看这件事项目搁置三周之后你自己也会忘掉的。还有一个我经常用的调试技巧如果前端页面迟迟看不到数据别急着翻代码可以直接在浏览器地址栏里输一遍后端接口地址比如http://localhost:8000/api/todos看直接访问返回什么。这一步能立刻区分问题是出在前端还是后端——如果后端能返回 JSON说明接口是通的问题一定在前端的请求封装或代理配置上如果连直接访问都报错那就安心去改后端代码吧。这个排查思路无数次帮我快速定位问题比一头扎进代码里盲猜高效太多。FastAPI 和 React 的组合在前后端分离的大趋势下已经是一套相当成熟的生产级方案。它没有很玄乎的设计没有复杂的魔法每一样技术都能看透、学透、用透。我写这篇东西不是想告诉你就该这么干而是把我在真实项目中撞过的墙、绕过的路、总结出来的做法原原本本晒出来。如果你正在做一个全新项目或者正在考虑从传统模板渲染切到前后端分离架构希望这套实践路线能帮你少走一段我在黑暗中走过的路。全栈的路从来不短但每踩过一个坑前面的路就会亮一点。