刚学FastAPI那会儿,几乎所有人都是把所有接口塞进一个 main.py 里,几十行代码,跑起来又快又爽。可项目一旦从玩具级别长成真正要上线的东西,这个文件就会变成一个谁都不想打开的黑洞------几百个接口挤在一起,改一个用户模块的逻辑,得先在几千行代码里翻半天才能找到位置。
这篇文章想聊的就是这件事,FastAPI项目变大之后,怎么把代码拆得清楚、拆得舒服,团队协作的时候不会互相踩脚。
为什么拆分不是可选项,而是必修课
小项目里,单文件的坏处不明显。可随着接口数量增长,几个问题会慢慢冒出来。
代码定位变难。想改用户注册逻辑,得先在几百个函数里用 Ctrl+F 搜索,效率极低。
团队协作容易冲突。两个人同时改一个文件,Git 合并的时候一堆冲突等着你。
测试和维护成本飙升。业务逻辑、数据校验、路由声明全揉在一起,写单元测试时很难做到精准隔离。
Reddit 上有个提问挺有代表性,一位开发者说自己在纠结到底该按功能分文件夹,还是按文件类型(models、routers、schemas各自成一个目录)分文件夹,两种方式各有各的烦恼。这其实是几乎所有中大型FastAPI项目都会遇到的十字路口。
拆分的基本积木:APIRouter
FastAPI官方给出的第一步解法叫 APIRouter,可以把它理解成一个"迷你版的FastAPI应用",专门负责管理某一部分的接口,最后再统一注册进主应用里。
先看一个最基础的例子,假设我们要把用户相关的接口单独拿出来。
python
# app/routers/users.py
from fastapi import APIRouter
router = APIRouter(
prefix="/users",
tags=["users"],
)
@router.get("/")
async def read_users():
return [{"username": "Rick"}, {"username": "Morty"}]
@router.get("/{username}")
async def read_user(username: str):
return {"username": username}
在主文件里,只需要把这个 router 导入并注册进来。
python
# app/main.py
from fastapi import FastAPI
from app.routers import users, items
app = FastAPI()
app.include_router(users.router)
app.include_router(items.router)
main.py 顿时清爽了很多,它只负责一件事------把各个模块的路由拼装起来,不再关心每个接口内部具体怎么实现。
用一张图来表示这层结构会更直观。
官方文档里还提到一个容易踩坑的细节,多个模块之间互相引用时要用显式的绝对导入 ,不要图省事用相对导入,否则项目稍微一改目录结构,一堆 ImportError 等着你排查。
两种主流拆分哲学,按类型 or 按业务域
有了 APIRouter 这块积木之后,真正的问题才浮出水面------这些积木该怎么摆放。业界大致分成两条路子。
路子一,按文件类型拆分
也就是所有的路由放进一个 routers/ 文件夹,所有的数据库模型放进 models/,所有的Pydantic校验模型放进 schemas/。这种方式在小项目里非常直观,新手一看就懂。
但当项目里同时存在用户、订单、支付、通知等十几个业务模块时,这种结构会带来一个很现实的麻烦------想改用户模块,得同时打开 routers/users.py、models/users.py、schemas/users.py,如果IDE里再开着其他模块的文件,屏幕上全是不相关的标签页,找起来眼花。
路子二,按业务域拆分
这种方式借鉴了Netflix开源项目Dispatch的思路,每个业务领域(auth、posts、payments......)自成一个文件夹,路由、模型、逻辑、异常处理全都收纳在这一个目录下。
两种思路放在一起对比会更清楚。
实践下来,小型项目或者微服务用第一种没什么问题,接口数量一旦上百,第二种的优势会越来越明显------改动被天然限制在一个文件夹里,新人接手某个业务模块也不用把整个项目翻个底朝天。
一份可以直接照着抄的项目骨架
zhanymkanov在他的FastAPI最佳实践仓库里给出了一套经过生产环境验证的目录结构,星标接近一万八千,算是社区里比较权威的参考之一。
bash
fastapi-project
├── alembic/
├── src
│ ├── auth
│ │ ├── router.py # 该模块所有接口
│ │ ├── schemas.py # pydantic校验模型
│ │ ├── models.py # 数据库模型
│ │ ├── dependencies.py # 该模块专属依赖
│ │ ├── config.py # 局部配置
│ │ ├── constants.py # 常量与错误码
│ │ ├── exceptions.py # 模块专属异常
│ │ └── service.py # 业务逻辑
│ ├── posts
│ │ └── ...(结构同上)
│ ├── config.py # 全局配置
│ ├── database.py # 数据库连接
│ └── main.py # 应用入口
├── tests/
│ ├── auth
│ └── posts
├── requirements/
└── alembic.ini
这套结构的几个设计原则值得拿出来单独说说。
所有业务代码统一放进 src/ 目录,src/main.py 是唯一的应用入口,负责把各个模块的路由组装起来。
每个业务模块内部结构高度一致,router.py 管接口,service.py 管逻辑,schemas.py 管数据校验,models.py 管数据库表结构,团队里每个人都能秒懂怎么找代码。
模块之间互相引用时,坚持用带模块名的显式导入,比如 from src.auth import constants as auth_constants,这样即使两个模块里都有个叫 constants 的文件,也不会互相打架。
测试目录的结构完全镜像业务目录,tests/auth 对应 src/auth,这个小细节能大幅降低维护测试的心智负担。
另一种思路,加上版本号的MVC风格
Stack Overflow上还有一个高赞回答,给出了一套更接近传统MVC模式的结构,特别适合需要做API版本管理的项目。
css
your_project
├── main.py
├── core
│ ├── models
│ │ └── database.py
│ ├── schemas
│ │ └── schema.py
│ └── settings.py
├── tests
│ └── v1
└── v1
├── api.py
└── endpoints
└── endpoint.py
这种结构把版本号直接体现在目录层级上,v1/ 文件夹装着第一代接口的所有内容,未来要发布 v2 时,直接新建一个平级目录,旧接口完全不受影响。对那些需要长期对外提供API、还要考虑兼容性的项目(比如开放平台、SaaS产品),这种做法会省不少心。
拆分之后,容易踩的几个坑
代码拆开只是第一步,真正让项目跑得顺畐的,是几个容易被忽视的细节。
循环引用问题。模块A引用模块B,模块B又反过来引用模块A,Python直接报错。解决办法通常是把共享的东西(比如公共依赖、公共异常类)提到更上层的公共目录,而不是让业务模块之间互相引用。
依赖注入的层级设计 。APIRouter 本身可以在声明的时候直接带上一批依赖项,比如统一的鉴权逻辑,这样这个路由下所有接口都会自动带上这层校验,不用每个接口函数里重复写一遍。
API版本管理。当接口需要长期维护多个版本时,提前规划好版本目录能省掉后期大量的重构成本。
测试目录与业务目录保持镜像。这看起来是个小习惯,但项目大到几十个模块之后,这种一致性能帮团队节省大量翻找测试文件的时间。
用一张图总结一下整个拆分的思考路径,可能会更清楚。

写在最后
代码拆分这件事,说到底没有放之四海皆准的唯一答案,核心原则是一致性 和可预测性------不管选哪种结构,团队里每个人都应该能凭直觉猜到某段代码大概会在哪个文件夹里。项目小的时候按类型分文件夹足够用,项目一旦跨越几十个业务模块的门槛,按业务域拆分几乎是必然的选择。
APIRouter 只是提供了拆分的工具,真正决定项目能不能长期健康演进的,是团队有没有在项目早期就定好一套大家都认同、并且愿意长期遵守的组织规则。
参考资料
FastAPI官方文档,Bigger Applications - Multiple Files,fastapi.tiangolo.com/tutorial/bi...
zhanymkanov,FastAPI Best Practices and Conventions,GitHub仓库,github.com/zhanymkanov...
Reddit r/FastAPI,FastAPI Large App Structure讨论帖,www.reddit.com/r/FastAPI/c...
Stack Overflow,What are the best practices for structuring a FastAPI project,stackoverflow.com/questions/6...