当FastAPI项目开始"膨胀",代码该往哪儿放

刚学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 顿时清爽了很多,它只负责一件事------把各个模块的路由拼装起来,不再关心每个接口内部具体怎么实现。

用一张图来表示这层结构会更直观。

graph TD A[main.py 应用入口] --> B[users路由模块] A --> C[items路由模块] A --> D[admin路由模块] B --> E[users业务逻辑] C --> F[items业务逻辑] D --> G[admin业务逻辑]

官方文档里还提到一个容易踩坑的细节,多个模块之间互相引用时要用显式的绝对导入 ,不要图省事用相对导入,否则项目稍微一改目录结构,一堆 ImportError 等着你排查。


两种主流拆分哲学,按类型 or 按业务域

有了 APIRouter 这块积木之后,真正的问题才浮出水面------这些积木该怎么摆放。业界大致分成两条路子。

路子一,按文件类型拆分

也就是所有的路由放进一个 routers/ 文件夹,所有的数据库模型放进 models/,所有的Pydantic校验模型放进 schemas/。这种方式在小项目里非常直观,新手一看就懂。

但当项目里同时存在用户、订单、支付、通知等十几个业务模块时,这种结构会带来一个很现实的麻烦------想改用户模块,得同时打开 routers/users.pymodels/users.pyschemas/users.py,如果IDE里再开着其他模块的文件,屏幕上全是不相关的标签页,找起来眼花。

路子二,按业务域拆分

这种方式借鉴了Netflix开源项目Dispatch的思路,每个业务领域(auth、posts、payments......)自成一个文件夹,路由、模型、逻辑、异常处理全都收纳在这一个目录下。

两种思路放在一起对比会更清楚。

graph LR subgraph 按类型拆分 T1[routers目录] T2[models目录] T3[schemas目录] T4[services目录] end subgraph 按业务域拆分 D1[auth模块] D2[posts模块] D3[payments模块] end

实践下来,小型项目或者微服务用第一种没什么问题,接口数量一旦上百,第二种的优势会越来越明显------改动被天然限制在一个文件夹里,新人接手某个业务模块也不用把整个项目翻个底朝天。


一份可以直接照着抄的项目骨架

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...

相关推荐
fliter1 小时前
程序员每天能省 1 小时的 50 个 macOS/终端/Git/浏览器技巧
后端
神奇小汤圆1 小时前
Redis为什么使用哈希槽而不用一致性哈希
后端
用户8356290780511 小时前
Python设置PowerPoint幻灯片背景的方法
后端·python
wang_yb1 小时前
Python 中 10 个最常用的统计函数
python·databook
databook1 小时前
Python 中 10 个最常用的统计函数
python·数据分析
站大爷IP2 小时前
Python 的 is 把我坑惨了,原来 == 和 is 在小整数池外完全是两码事
后端
用户40966601317512 小时前
Jackson 序列化:@JsonIgnore / @JsonProperty / @JsonFormat / @JsonInclude / @JsonUnwrapped 一次讲清楚
java·后端
雨落倾城夏未凉2 小时前
halcon核心-图像预处理(四)
后端
用户233376852182 小时前
一张损坏JPEG文件的背后排查
后端