写后端接口写惯了的人,第一次琢磨怎么把前端也塞进 FastAPI 项目时,往往会犯迷糊。是把 React 打包完的 dist 目录扔进后端项目里,还是干脆前后端分家各跑各的?这篇文章就把这个问题拆开揉碎讲清楚,顺带聊聊大型项目里前后端代码该怎么摆放才不会让后来接手的同事骂人。
🧭 FastAPI 托管前端的两种姿势
FastAPI 官方给出了两条路子,一条是新出的 app.frontend() ,专门为现代前端框架的构建产物量身定制;另一条是老牌的 StaticFiles,更偏向通用静态资源托管。两者思路不同,适用场景也不太一样。
app.frontend():专为 SPA 而生
如果你的前端是用 Vite 打包的 React、Vue、Svelte 或者 Astro 之类的框架,构建完之后会生成一个类似 dist 的目录,里面躺着 index.html 和一堆 assets。这种场景下,app.frontend() 就是量身定做的方案。
它的基本用法非常简洁
python
from fastapi import FastAPI
app = FastAPI()
app.frontend("/", directory="dist")
FastAPI 内部有个很聪明的处理逻辑,会先检查请求是否命中了你写的路径操作(也就是那些 @app.get、@app.post 装饰的接口),只有没匹配上的时候才会去翻前端目录里的文件。这意味着你完全不用担心前端托管会把 API 路由给覆盖掉。
项目结构大致长这样
markdown
.
├── pyproject.toml
├── app
│ ├── __init__.py
│ └── main.py
└── dist
├── index.html
└── assets
└── app.js
请求 /assets/app.js 会精准命中 dist/assets/app.js,一一对应,不需要额外配置。
客户端路由的坑:为什么需要 fallback
单页应用(SPA)有个特点,像 /dashboard/settings 这样的路径其实压根不是磁盘上真实存在的文件,而是前端框架用 JavaScript 在浏览器里模拟出来的路由。如果用户直接在地址栏敲这个 URL 或者刷新页面,后端如果傻乎乎地去找这个文件,自然会返回 404。
解决办法是让后端在找不到对应文件时,退而返回 index.html,把接管权交给前端框架自己去处理路由
python
from fastapi import FastAPI
app = FastAPI()
app.frontend("/", directory="dist", fallback="index.html")
这里有个细节挺讲究的,FastAPI 只会对明确表示接受 HTML(带 Accept: text/html 或 Accept: application/xhtml+xml 头)的 GET 和 HEAD 请求做这个 fallback,这恰好和浏览器发起页面导航时的行为一致。至于缺失的 JS、CSS、图片文件,该 404 还是 404,不会被误伤。
用 POST 、PUT 之类的方法请求一个只匹配前端 fallback 的路径,同样会规规矩矩地返回 404。常规的 FastAPI 路径操作永远优先级更高。
静态 404 页面与自动 Fallback
有些前端工具比如 Astro,会给每个页面单独生成静态 HTML 文件,这种情况下你可能更想要一个专门的 404 页面而不是让所有找不到的路径都跳回首页
python
app.frontend("/", directory="dist", fallback="404.html")
响应状态码依然保持 404,只是内容换成了这个自定义页面。
其实 FastAPI 默认用的是 fallback="auto",逻辑挺贴心
- 如果目录里有
404.html,缺失路径就返回它,状态码 404 - 否则如果有
index.html,浏览器导航请求就返回它,配合客户端路由
所以大多数时候你压根不用手动指定 fallback 参数,写个 app.frontend("/", directory="dist") 就够了。如果你压根不想要任何 fallback 行为,设置 fallback=None 就能让缺失路径老老实实返回普通 404。
开发环境的贴心检查
app.frontend() 还有个 check_dir 参数,默认是 "auto"。当环境变量 FASTAPI_ENV 设为 development 时(用 fastapi dev 命令启动会自动帮你设置),如果前端构建目录还没生成,FastAPI 只会警告一下,不会报错------这样你就能在前端还没跑 npm run build 之前先把后端跑起来调试。但换到其他环境(比如生产环境),目录缺失就直接抛错,能帮你在部署前及早发现配置问题。
🔧 StaticFiles:更通用的老办法
如果你只是想托管一些静态资源,比如图片、CSS,或者你的前端并不是那种典型的 SPA 构建产物,StaticFiles 依然是一个稳妥的选择。用法是通过 app.mount() 把某个 URL 路径挂载到本地目录
python
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
这行代码干的事情很直白,把 /static 这个路径前缀映射到本地的 static 文件夹,访问 /static/logo.png 就能拿到 static/logo.png 这个文件。
需要注意的是,mount() 是整体挂载,不像路径操作那样能细粒度控制,官方文档也建议如果是要托管一整个前端应用,优先用 app.frontend(),更省心。
StaticFiles 和 app.frontend() 的选择,本质上是场景问题。下面用一张图梳理一下决策逻辑

🏗️ 大型全栈项目的代码组织之道
聊完了托管机制,更实际的问题来了。真正的项目往往不止一个前端一个后端这么简单,可能还有管理后台、移动端 H5、多个微服务。这时候代码怎么摆放,直接决定了团队协作的顺畸程度。
单体仓库(Monorepo):前后端同居一室
如果项目规模不算特别夸张,把前后端放进同一个 Git 仓库是个相当省心的选择。构建流程、版本发布、代码评审都能统一管理,不用在多个仓库之间来回切换。
一个典型的目录结构可能是这样
arduino
my-project/
├── backend/
│ ├── app/
│ │ ├── __init__.py
│ │ ├── main.py
│ │ ├── api/
│ │ ├── models/
│ │ └── services/
│ ├── pyproject.toml
│ └── tests/
├── frontend/
│ ├── src/
│ ├── public/
│ ├── package.json
│ └── vite.config.ts
├── frontend/dist/ # 构建产物,被后端引用
├── docker-compose.yml
└── README.md
后端在 main.py 里用 app.frontend("/", directory="../frontend/dist") 把前端构建产物接进来,或者用 Docker 多阶段构建把前端打包好的文件复制进后端镜像,部署时就是一个整体。
这种结构的好处很实在。团队小的时候,前后端开发者能在同一个 PR 里协同修改接口和调用逻辑,不用在两个仓库之间反复提 issue 对齐版本号。
多仓库(Polyrepo):各自为战但保持契约
项目大到一定程度,尤其是多个前端团队共用同一套后端 API,或者后端要拆成好几个独立部署的服务时,拆成多个仓库反而更清晰。这时候前后端之间的契约就变得至关重要,通常靠 OpenAPI 规范文档来对齐,FastAPI 天生自带的自动生成 OpenAPI schema 能力在这种场景下就特别有用。
多仓库结构下,前端通常独立部署到 CDN 或者静态托管平台,后端只负责提供 API,不再操心托管 HTML 文件的事。这种情况下,app.frontend() 基本用不上,后端更专注于纯 API 服务,配合 CORS 中间件让前端能跨域调用即可。
架构决策:怎么选
两种模式各有取舍,下面这张表格能帮你快速对照
| 维度 | Monorepo(单仓库) | Polyrepo(多仓库) |
|---|---|---|
| 适用规模 | 中小型项目,团队人数少 | 大型项目,多团队协作 |
| 部署方式 | 前后端打包为一体或同容器 | 前端CDN独立部署,后端独立扩容 |
| 版本管理 | 前后端版本天然同步 | 需要靠API版本号/契约对齐 |
| 前端托管 | 常用 app.frontend() | 通常不用,前端独立静态托管 |
| 协作复杂度 | 较低,统一评审流程 | 较高,需要跨仓库沟通接口变更 |
| 典型场景 | 内部管理系统、中小型SaaS | 大厂多端产品、开放平台API |
用图理清整体架构关系
如果是采用 Monorepo 并借助 app.frontend() 做整体托管,请求流转的逻辑大致如下
这张图能清楚看出,FastAPI 处理请求时始终把 API 路径操作放在第一优先级,前端文件的托管逻辑只是补位角色,压根不会打扰你写好的业务接口。
💡 实践建议与小结
聊到这里,基本的技术脉络已经梳理清楚了。简单归纳一下
- 中小型项目、内部工具 这类场景,直接用
app.frontend()把前后端揉进一个 FastAPI 应用是最省心的做法,配置简单,部署也是一体化的,不用操心跨域问题 - 零散静态资源 (比如上传的图片、文档)更适合用
StaticFiles单独挂载,灵活度更高 - 大型多团队项目建议拆成 Polyrepo,前端独立走 CDN 部署,后端专注做纯 API 服务,靠 OpenAPI 契约维系前后端的协作关系
- 无论哪种模式,记得利用 FastAPI 的
check_dir="auto"特性,在开发阶段先把后端跑起来,不用死等前端构建完成才能调试
技术选型这事儿没有放之四海而皆准的答案,归根结底还是要看团队规模、部署环境和长期维护成本这几个变量。小项目图个省事儿直接一体化,大项目图个清晰边界拆开走,这是个挺朴素的道理。
参考资料
FastAPI 官方文档 - 前端托管. fastapi.org.cn/tutorial/fr...
How to serve static files in FastAPI - Stack Overflow. stackoverflow.com/questions/6...
FastAPI 官方文档 - StaticFiles 参考. fastapi.tiangolo.com/reference/s...
Serving with speed: Static Files in FastAPI - Medium. medium.com/featurepren...