FastAPI 项目开始变大后,如何用 APIRouter 拆分代码
摘要:当模型、存储和路由挤进同一个 main.py,先用测试固定行为,再通过 APIRouter、Python 包和清晰的依赖方向完成一次可验证重构。

图:单文件中的职责被整理成路由、数据模型和存储模块,外部接口保持不变。
单文件 FastAPI 在几十行时很舒服,到了 CRUD、认证和数据库一起出现时,修改一个模型却要在几百行代码里来回跳。问题不是文件长,而是不同变化原因混在了一起。
下面不新增任何接口,只做一次重构:任务模型、存储、路由和应用入口各回到自己的位置。重构前后仍然访问 /tasks,测试负责证明外部行为没有变化。
拆分以后,目录应该一眼能看懂
ch06_routers/
├── main.py
└── app/
├── main.py
├── schemas.py
├── store.py
└── routers/
└── tasks.py
fastapi dev examples/ch06_routers/main.py
原有 /tasks 接口继续工作,/docs 中任务接口归入 tasks 分组。
重构之前,先留下一条安全绳
本文的重构目标是保持外部行为不变。在动手修改代码之前,先用已有的测试确认旧版接口全部通过,这样重构后才能对比结果。
如果你是按顺序从上一篇读下来,可以直接运行上一章的 CRUD 接口测试:
cd 04-fastapi-beginner
source .venv/bin/activate
python -m pytest tests/test_ch05.py -q
如果你是从公众号单独阅读本文,请先完成本系列第 05 篇或直接进入示例目录确认代码可运行。
确认测试通过后,打开 示例目录 开始重构。
按变化原因拆出路由、模型和存储
创建任务路由器
router = APIRouter(prefix="/tasks", tags=["tasks"])
@router.post("", response_model=TaskRead, status_code=201)
def create_task(payload: TaskCreate):
return store.add_task(payload.title)
路由装饰器从 @app 变为 @router。公共前缀只声明一次,标签会进入 OpenAPI 文档。
组装应用
from fastapi import FastAPI
from .routers import tasks
app = FastAPI(title="任务管理 API", version="0.6.0")
app.include_router(tasks.router)
入口只负责创建应用和组装模块;任务细节留在任务模块。
使用相对导入
from .. import store
from ..schemas import TaskCreate, TaskRead
两个点表示从 routers 返回上一级 app 包。使用模块方式启动应用,Python 才能正确理解包关系。

图:按职责拆分模块后,每一类变化都有清楚的落点。
接口地址没变,重构才算完成
fastapi dev examples/ch06_routers/main.py
curl -X POST http://127.0.0.1:8000/tasks \
-H "Content-Type: application/json" -d '{"title":"拆分路由"}'
curl http://127.0.0.1:8000/tasks/1
curl http://127.0.0.1:8000/health
预期三个请求分别为 201、200、200。自动验证路由行为和 OpenAPI 标签:
python -m pytest tests/test_ch06.py -q

图:重构改变的是内部组织方式,对外地址和响应契约应保持不变。
APIRouter 是路由集合,不是新服务
拆分依据是变化原因:schemas 随接口契约变化,store 随存储策略变化,router 随 HTTP 行为变化,main 随应用组装变化。目录不是越深越专业;只有当职责已经出现时才拆分。
APIRouter 可以理解为"尚未挂载到最终应用的路由集合"。include_router 把集合合入应用,最终客户端仍然只面对一个服务。
根目录的 main.py 只是教学启动入口,正式项目也可以在 pyproject.toml 声明 entrypoint。
包导入和 prefix 最容易出错
attempted relative import:直接运行包内文件;改用教程给出的 FastAPI 启动命令。- 忘记
include_router:代码存在但/docs没有路由。 - prefix 两边重复
/tasks:最终地址变成/tasks/tasks。 - 循环导入:schemas 不应反向导入 main;依赖方向应指向更稳定模块。
- 为每个函数创建一个文件:导航成本会超过拆分收益。
再拆一个 system 路由试试
- 新增
routers/system.py,把/health移进去。 - 为所有任务路由统一添加
responses={404: ...}文档。 - 故意移除
include_router,用测试定位失败。 - 画出 main、router、schemas、store 的导入方向,检查是否有环。
最后,用测试固定这次改动
手动请求通过后,运行与本文对应的自动化测试:
python -m pytest tests/test_ch06.py -q
python tests/validate_course.py
写在最后
路由拆分解决了文件职责问题,但分页、数据库会话和鉴权仍会在多个路由重复。下一篇使用 FastAPI 依赖注入表达这些公共前置条件。