第 19 章 · 大型项目结构拆分
本章目标:学会用
APIRouter把单文件应用拆成可维护的多模块工程,理解官方推荐的目录结构与依赖共享方式。
19.1 为什么需要 APIRouter
写真实项目时不可能把所有路径操作堆在一个 main.py 里。FastAPI 的答案是 APIRouter——可以把它理解为"迷你 FastAPI":支持与 FastAPI 类完全相同的参数(dependencies、tags、responses 等),先在各自模块里声明路由,最后统一"装回"主应用。如果你用过 Flask,它相当于 Blueprint。
19.2 官方推荐的项目结构
.
├── app
│ ├── __init__.py # 使 app 成为 Python 包
│ ├── main.py # 主模块,把所有 router 组装起来
│ ├── dependencies.py # 跨 router 共享的依赖
│ ├── routers # 路由子包
│ │ ├── __init__.py
│ │ ├── items.py # /items 相关路径操作
│ │ └── users.py # /users 相关路径操作
│ └── internal # 内部子包(如管理接口)
│ ├── __init__.py
│ └── admin.py要点:
- 每个目录都要有
__init__.py,使其成为 Python 包,才能相互导入; app/main.py是组装点,业务逻辑分散到各模块后它会非常薄;- 共享依赖放
app/dependencies.py,而不是散落在各 router 文件里。
19.3 编写一个 router 模块
app/routers/users.py——只关心用户的路由:
from fastapi import APIRouter, Depends, HTTPException
# 单个点:从当前包(app/routers/)里找,这里不存在 dependencies.py
# 两个点:回到上一级包 app/ 找 app/dependencies.py ✅
from ..dependencies import get_token_header
# prefix 会加到本 router 内所有路径前;tags 用于文档分组
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/")
async def read_users():
return [{"username": "alice"}, {"username": "bob"}]
@router.get("/me")
async def read_current_user(token: str = Depends(get_token_header)):
return {"token": token}app/routers/items.py——演示 router 级公共配置:
from fastapi import APIRouter, Depends, HTTPException
from ..dependencies import get_token_header, get_query_token
router = APIRouter(
prefix="/items", # 注意:不能以 / 结尾
tags=["items"],
dependencies=[Depends(get_token_header)], # 本组全部路由都要求 X-Token
responses={404: {"description": "Not found"}}, # 预定义响应进文档
)
@router.get("/") # 实际路径 /items/
async def read_items():
return [{"item_id": "Foo"}]
@router.get("/{item_id}") # 实际路径 /items/{item_id}
async def read_item(item_id: str):
return {"item_id": item_id}prefix 细节
每个路径操作的路径必须以 / 开头,所以 prefix 不能以 / 结尾(写 /items 不写 /items/)。router 级 dependencies 对整组路由生效且先于路由内依赖执行——典型用途是给一整组接口批量加认证。
19.4 在主应用中组装
app/main.py 只负责组装:
from fastapi import Depends, FastAPI
from .routers import items, users
from .dependencies import get_query_token
# 全局依赖会与各 router 的依赖叠加生效
app = FastAPI(dependencies=[Depends(get_query_token)])
app.include_router(items.router)
app.include_router(users.router)
@app.get("/", tags=["root"])
async def root():
return {"message": "Hello Bigger Applications!"}运行方式不变:
uv run fastapi dev app/main.pyinclude_router() 还可以临时覆盖配置:
# 再次挂载同一 router 到不同前缀,并追加标签
app.include_router(items.router, prefix="/v1", tags=["v1"])同一 router 可以多次 include(比如同时暴露 /items 与 /v1/items),每个具体路径操作上还能再叠加额外的 tags 和 responses(最终 tags 是合并结果)。
19.5 相对导入与循环导入
相对导入规则
from .dependencies import ...:在当前包里找(对app/routers/items.py来说是app/routers/,通常找不到你要的文件);from ..dependencies import ...:回上一级包app/找app/dependencies.py✅;from ...xxx import ...:回两级,会跳出app包直接报错。
避免循环导入
拆分多文件后最常见的崩溃是循环导入:main.py → routers/users.py → main.py。规避手段:
- 依赖下沉:把被多个 router 共享的依赖、模型、工具函数移到独立模块(
dependencies.py、models.py),谁需要谁导入,绝不反向导入main.py; - router 不 import app:router 模块只创建自己的
APIRouter,组装动作只发生在main.py; - 类型提示延迟导入:确需引用主应用对象时,放到函数体内
import,或使用TYPE_CHECKING块。
# app/routers/users.py —— 错误示范(不要这样做)
# from ..main import app # ❌ main 又 import 了本模块 → 循环导入
# 正确姿势:只导出 router,让 main 来 include
router = APIRouter(prefix="/users")19.6 本章小结
APIRouter是"迷你 FastAPI",支持prefix/tags/dependencies/responses等全套参数;- 官方结构:
main.py组装 +routers/放路由 +dependencies.py放共享依赖 + 各目录带__init__.py; prefix不能以/结尾;router 级依赖对整组路由生效、执行顺序先于路由级依赖;- 同一 router 可多次
include_router挂到不同前缀; - 用"依赖下沉到独立模块 + router 不反向 import 主应用"避免循环导入。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. 在 app/routers/items.py 中,正确的共享依赖导入写法是?
2. 关于 APIRouter 的 prefix,下列哪个是合法且符合规范的?
3. router 级 dependencies=[Depends(get_token)] 的效果是?
4. 以下哪种做法最容易引发循环导入?
🛠️ 动手实践
- 把第 13 章的数据库 CRUD 应用拆成
app/routers/todos.py+app/database.py+app/models.py三层结构,main.py只剩组装代码。 - 给
todosrouter 加上prefix="/api/todos"和 router 级dependencies=[Depends(verify_api_key)],验证没有 key 时整组接口都返回 403。 - 故意在 router 文件里
from ..main import app制造一次循环导入,观察报错信息,然后按 19.5 的方法修复。
工程搭好了,接下来给它配上自动化测试:第 20 章 · 测试 FastAPI 应用。