Skip to content

第 19 章 · 大型项目结构拆分

本章目标:学会用 APIRouter 把单文件应用拆成可维护的多模块工程,理解官方推荐的目录结构与依赖共享方式。

19.1 为什么需要 APIRouter

写真实项目时不可能把所有路径操作堆在一个 main.py 里。FastAPI 的答案是 APIRouter——可以把它理解为"迷你 FastAPI":支持与 FastAPI 类完全相同的参数(dependenciestagsresponses 等),先在各自模块里声明路由,最后统一"装回"主应用。如果你用过 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——只关心用户的路由:

python
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 级公共配置:

python
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 只负责组装:

python
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!"}

运行方式不变:

bash
uv run fastapi dev app/main.py

include_router() 还可以临时覆盖配置:

python
# 再次挂载同一 router 到不同前缀,并追加标签
app.include_router(items.router, prefix="/v1", tags=["v1"])

同一 router 可以多次 include(比如同时暴露 /items/v1/items),每个具体路径操作上还能再叠加额外的 tagsresponses(最终 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。规避手段:

  1. 依赖下沉:把被多个 router 共享的依赖、模型、工具函数移到独立模块(dependencies.pymodels.py),谁需要谁导入,绝不反向导入 main.py
  2. router 不 import app:router 模块只创建自己的 APIRouter,组装动作只发生在 main.py
  3. 类型提示延迟导入:确需引用主应用对象时,放到函数体内 import,或使用 TYPE_CHECKING 块。
python
# 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. 以下哪种做法最容易引发循环导入?

🛠️ 动手实践

  1. 把第 13 章的数据库 CRUD 应用拆成 app/routers/todos.py + app/database.py + app/models.py 三层结构,main.py 只剩组装代码。
  2. todos router 加上 prefix="/api/todos" 和 router 级 dependencies=[Depends(verify_api_key)],验证没有 key 时整组接口都返回 403。
  3. 故意在 router 文件里 from ..main import app 制造一次循环导入,观察报错信息,然后按 19.5 的方法修复。

工程搭好了,接下来给它配上自动化测试:第 20 章 · 测试 FastAPI 应用