首页
/ FastAPI 大型应用组织实战:多文件结构、APIRouter 模块化拆分与统一装配

FastAPI 大型应用组织实战:多文件结构、APIRouter 模块化拆分与统一装配

2026-09-06 18:09:29作者:齐添朝

FastAPI 允许(甚至鼓励)把接口逻辑分散到多个 Python 模块中,再通过 APIRouterapp.include_router() 把分散的路径操作(path operations)装配回主应用,在保持全部灵活性(前缀、标签、依赖、响应声明)的同时把代码组织得清晰可维护。本文以官方教程 Bigger Applications - Multiple Files 为主体,结合仓库中的可运行示例源码与完整测试用例,讲解如何用"Python 包 + 子模块 + APIRouter"的结构组织一个中型 FastAPI 应用,并回答前缀如何设置、相对导入 from ..dependencies import ... 为何不能多一个点、依赖的执行顺序如何、第三方共享 Router 如何不改源码就加前缀等实战细节。

为什么需要多文件结构

在构建一个真正的应用或 Web API 时,几乎不可能把所有内容塞进单个文件——用户接口、商品接口、后台管理接口、公共依赖会迅速膨胀。FastAPI 为此提供了专门的工具:APIRouter。它允许把逻辑分模块编写,同时所有模块仍然是同一个 FastAPI 应用(同一个 "Python Package")的一部分。

从 Flask 转过来的开发者可以把它理解为 Flask 的 Blueprints(蓝图)。FastAPI 文档给出的定位是:APIRouter 是一个 "mini FastAPI" 类,FastAPI 类上支持的 parametersresponsesdependenciestags 等选项,它全部支持。

推荐目录结构:一个 Python 包的应用

教程给出的典型结构如下(其中的注释部分来自官方文档原文):

.
├── app                  # "app" 是一个 Python 包
│   ├── __init__.py      # 本文件让 "app" 成为 "Python package"
│   ├── main.py          # "main" 模块,例如 from app.main import app
│   ├── dependencies.py  # "dependencies" 模块,例如 import app.dependencies
│   └── routers          # "routers" 是一个 "Python 子包"
│   │   ├── __init__.py  # 让 "routers" 成为 "Python 子包"
│   │   ├── items.py     # "items" 子模块,例如 import app.routers.items
│   │   └── users.py     # "users" 子模块,例如 import app.routers.users
│   └── internal         # "internal" 是一个 "Python 子包"
│       ├── __init__.py  # 让 "internal" 成为 "Python 子包"
│       └── admin.py     # "admin" 子模块,例如 import app.internal.admin
[![多文件结构的 Python 包划分示意图](https://raw.gitcode.com/GitHub_Trending/fa/fastapi/raw/50113da16fec53b66b80d75e80a89296de4fa5a5/docs/en/docs/img/tutorial/bigger-applications/package.drawio.svg?utm_source=gitcode_repo_files)](https://gitcode.com/GitHub_Trending/fa/fastapi?utm_source=gitcode_repo_files)

注意每个目录/子目录下都有 __init__.py,正是这些文件让目录成为 Python 包,从而实现模块间的互相导入。例如在 app/main.py 里可以写 from app.routers import items。逐层解读这套命名空间:

  • app 目录内包含空文件 app/__init__.py,因此它是一个 "Python 包",名字就是 app
  • app/main.py 位于包内,是包 app 的一个 "模块",全名 app.main
  • app/dependencies.py 同理,模块全名 app.dependencies
  • 子目录 app/routers/ 带有自己的 __init__.py,构成 "Python 子包" app.routers
  • app/routers/items.py 是子模块 app.routers.itemsapp/routers/users.py 是子模块 app.routers.users
  • app/internal/ 是另一个子包 app.internal,其中 app/internal/admin.py 是子模块 app.internal.admin

这套示例结构在本仓库中有对应的完整可运行实现,位于 docs_src/bigger_applications/app_an_py310/(含 main.pydependencies.pyrouters/{users,items}.pyinternal/admin.py 及各自的 __init__.py),配套的自动化测试在 tests/test_tutorial/test_bigger_applications/test_main.py

第一个模块:用 APIRouter 声明用户路径操作

假设 app/routers/users.py 专门负责用户相关的路径操作。引入 APIRouter 并实例化它,方式与实例化 FastAPI 类完全一致:

from fastapi import APIRouter

router = APIRouter()

教程示例中变量名取 router,你也可以随意命名,它只是一个普通对象。之后就用它声明路径操作,用法与直接用 FastAPI 类一模一样:

@router.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "Rick"}, {"username": "Morty"}]


@router.get("/users/me", tags=["users"])
async def read_user_me():
    return {"username": "fakecurrentuser"}


@router.get("/users/{username}", tags=["users"])
async def read_user(username: str):
    return {"username": username}

完整文件见 docs_src/bigger_applications/app_an_py310/routers/users.pyAPIRouter 的类实现在 fastapi/routing.py(继承自 Starlette 的 Router 并叠加 FastAPI 的依赖注入、OpenAPI 生成能力)。所有这些"子路由"最终要被装配进主 FastAPI 应用,在那之前,先看应用里会被多处复用的依赖。

公共依赖模块 app/dependencies.py

教程中的应用需要若干依赖在多个地方使用,于是把它们放进独立的模块 app/dependencies.py。示例中先实现一个读取自定义请求头 X-Token 的简单依赖:

from typing import Annotated

from fastapi import Header, HTTPException


async def get_token_header(x_token: Annotated[str, Header()]):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")


async def get_query_token(token: str):
    if token != "jessica":
        raise HTTPException(status_code=400, detail="No Jessica token provided")

docs_src/bigger_applications/app_an_py310/dependencies.py。其中:

  • get_token_header 读取自定义头 X-Token,值不是 fake-super-secret-token 就抛出 400;
  • get_query_token 读取查询参数 token,值不是 jessica 就抛出 400。

文档特别提醒:这里为了演示用了"自造的"请求头;真实项目中应优先使用 FastAPI 内置的 Security 工具(安全性依赖)(如 OAuth2PasswordBearerHTTPBearerAPIKeyHeader 等),它们会提供更完善的 OpenAPI 安全方案描述与交互文档支持。

第二个模块:APIRouter 级的 prefix、tags、responses、dependencies

处理 "items" 的接口位于 app/routers/items.py,包含两个路径操作:/items//items/{item_id}。为了让代码更聪明、去重更彻底,教程观察到该模块内所有路径操作共享同样的特征:

  • 路径前缀 prefix/items
  • tags:只有一个标签 items
  • 额外 responses 声明;
  • dependencies:都需要上面创建的 X-Token 依赖。

与其在每个路径操作上重复书写,不如直接把这些声明放在 APIRouter 上:

from fastapi import APIRouter, Depends, HTTPException

from ..dependencies import get_token_header

router = APIRouter(
    prefix="/items",
    tags=["items"],
    dependencies=[Depends(get_token_header)],
    responses={404: {"description": "Not found"}},
)


fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}


@router.get("/")
async def read_items():
    return fake_items_db


@router.get("/{item_id}")
async def read_item(item_id: str):
    if item_id not in fake_items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"name": fake_items_db[item_id]["name"], "item_id": item_id}


@router.put(
    "/{item_id}",
    tags=["custom"],
    responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
    if item_id != "plumbus":
        raise HTTPException(
            status_code=403, detail="You can only update the item: plumbus"
        )
    return {"item_id": item_id, "name": "The great Plumbus"}

完整文件见 docs_src/bigger_applications/app_an_py310/routers/items.py

前缀必须不带尾部斜杠

每个路径操作自身的路径必须以 / 开头,例如:

@router.get("/{item_id}")
async def read_item(item_id: str):
    ...

因此 prefix 不能以 / 结尾。这里的 prefix 应为 /items(而不是 /items/),装配后真实路由是 /items//items/{item_id}

Router 级声明对内部每个路径操作生效

  • tags:所有路径操作都会被标上 "items" 标签,这对基于 OpenAPI 的自动交互文档尤其有用(分组展示);
  • responses:所有路径操作都会带上预定义的额外响应;
  • dependencies:Router 级依赖列表会在每个请求到达时被求解/执行。如果某个具体路径操作自己也声明了依赖,两者都会执行

依赖的执行顺序

教程明确给出的执行次序是:

  1. 先执行 APIRouter 上声明的依赖;
  2. 再执行路径操作装饰器中的 dependencies(路径操作装饰器依赖)
  3. 最后执行普通参数依赖(即通过函数参数 + Depends() 注入的依赖)。

另外,你还可以在此基础上叠加带 scopesSecurity 依赖(即 OAuth2 作用域)用于细粒度授权。一个典型用途:把"认证"做成 APIRouter 级依赖,从而一次性保护整组路径操作,即使它们各自没有单独声明。prefixtagsresponsesdependencies 这些参数本质上都是 FastAPI 帮助开发者消除代码重复的特性。

注意:与 路径操作装饰器中的依赖 一致,Router 级依赖的返回值不会被传给路径操作函数体。

相对导入:...... 分别指到哪里

items.py 位于模块 app.routers.items(文件 app/routers/items.py),而依赖函数位于模块 app.dependencies(文件 app/dependencies.py),因此需要向上跳一级,使用 ..

from ..dependencies import get_token_header

理解相对导入的规则很重要。假如用了单点:

from .dependencies import get_token_header

含义是:从当前模块(app/routers/items.py)所在的包(目录 app/routers/)出发,找名为 dependencies 的模块——即想象中的 app/routers/dependencies.py——并导入其中的 get_token_header。但该文件不存在,依赖在 app/dependencies.py,所以会失败。

改用双点 ..

from ..dependencies import get_token_header

含义是:从当前模块所在包(app/routers/)出发,先回到父包(目录 app/),在那里找 dependencies 模块(即 app/dependencies.py),导入 get_token_header。这样就能正确工作。

若误用三点 ...

from ...dependencies import get_token_header

含义是:从 app/routers/ 出发,回到父包 app/,再回到 app/ 的父包——但 app 已经是最顶层,不存在再上一级包,所以示例中会直接报错。理解了点数与包层级的关系后,再复杂的相对导入也能写对。

在单个路径操作上继续叠加 tags 与 responses

Router 级声明不阻止你给某个具体路径操作叠加更多配置。items.py 里的 PUT /{item_id} 就是例子:它额外写了 tags=["custom"]responses={403: {...}}。最终:

  • 该路径操作的标签是两者合并的结果:["items", "custom"]
  • 它在文档中同时展示 404 与 403 两组额外响应描述;
  • 它仍然共享 Router 级的 prefix="/items"dependencies=[Depends(get_token_header)]responses={404: ...}

测试用例 test_puttest_put_forbidden(见 tests/test_tutorial/test_bigger_applications/test_main.py)验证了 PUT 语义:更新 plumbus 成功返回 200,更新其他 item 返回 403。

主应用 app/main.py:把一切装配起来

随着业务逻辑分散到各专属模块,主文件会变得非常简单——它只负责创建 FastAPI 实例并把各 Router 装配进来。完整代码如下:

from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

docs_src/bigger_applications/app_an_py310/main.py

全局依赖与 Router 依赖如何组合

创建 FastAPI 时同样可以声明[全局依赖](此处 dependencies=[Depends(get_query_token)]),它会与每个 APIRouter 各自的依赖组合、共同生效。因此在本示例中,任何请求都必须带 ?token=jessica(由 app 全局依赖 get_query_token 强制),而 /items/*/admin/* 的请求还必须额外带 X-Token 头。测试 tests/test_tutorial/test_bigger_applications/test_main.py 中大量 422(缺 token / 缺 X-Token)、400(token 错误或头错误)用例正是对这一组合行为的逐点验证。

导入 Router 子模块的方式与名称冲突规避

users.pyitems.pymain.py 同属 Python 包 app,可以用单点相对导入:

from .routers import items, users

含义:从当前模块(app/main.py)所在包(目录 app/)出发,找子包 routers(目录 app/routers/),从中导入子模块 itemsapp/routers/items.py)与 usersapp/routers/users.py)。相对地,写成 from app.routers import items, users 则是绝对导入,效果相同。关于 Python 包与模块的完整知识,可参阅 Python 官方 Modules 文档。

教程特别强调一个反模式:两个子模块里都有一个名为 router 的变量。如果逐个导入变量:

from .routers.items import router
from .routers.users import router

后导入的 users.router 会覆盖掉 items.router,两者便无法同时使用。因此应直接导入子模块,再通过 items.routerusers.router 访问各自的路由器对象,避免名称冲突。

include_router 加入 users 与 items

users.router 封装的是 app/routers/users.py 内的 APIRouteritems.router 同理。调用:

app.include_router(users.router)
app.include_router(items.router)

app.include_router() 会把该 Router 的所有路由并入主应用。

技术细节(官方文档说明):当 Router 被并入主应用后,FastAPI 会保持原来的 APIRouter 及其 APIRoute 持续活跃——这意味着自定义的 APIRouter / APIRoute 子类在并入之后仍可参与路由处理。同时官方明确表示不需要担心性能:这种设计是轻量的,旨在避免给每个请求增加额外开销,因此不会影响性能。

不改动第三方 Router 源码,在 include 时叠加 prefix/tags/dependencies/responses

考虑一个更贴近真实团队协作的场景:app/internal/admin.py 是组织内多个项目共享的 APIRouter(示例里只包含一个极简的路径操作),我们不能修改它来加 prefixdependenciestags 等:

from fastapi import APIRouter

router = APIRouter()


@router.post("/")
async def update_admin():
    return {"message": "Admin getting schwifty"}

docs_src/bigger_applications/app_an_py310/internal/admin.py。但我们仍希望:本项目中该 Router 的所有路径都以 /admin 开头、用本项目的 get_token_header 依赖保护,并带上 admin 标签与额外响应。这些全部可以在装配时传入 app.include_router()

app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)

效果是:在本应用中,admin 模块的每个路径操作都会获得前缀 /admin、标签 admin、依赖 get_token_header、以及额外响应 418(I'm a teapot)。原始 APIRouter 对象保持原样,因此同一个 app/internal/admin.py 仍可原封不动地共享给组织内的其他项目——其他项目完全可以给它配上不同的认证方式。

FastAPI.include_router 的完整签名定义在 fastapi/applications.pyprefix(可选路径前缀,默认 "")、tags(应用到该 Router 全部路径操作的标签列表,用于 OpenAPI,如 /docs 可见)、dependencies(应用到该 Router 全部路径操作的 Depends() 列表)、responses(额外展示在 OpenAPI 的响应)等关键字参数均为可选。APIRouter.include_router 与其对应实现位于 fastapi/routing.py,即 Router 套 Router 的机制。

测试 tests/test_tutorial/test_bigger_applications/test_main.py 中的 test_admin / test_admin_invalid_header 验证:带合法 token 与 X-TokenPOST /admin/ 返回 200,头无效时返回 400。

也可以直接在主应用上加路径操作

include_router 之外,你仍可直接在 FastAPI 实例上添加路径操作。示例里为了演示这一能力加了一个根路由:

@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

它与 include_router() 加入的所有路径操作和谐共存。test_root_token_jessica 等测试验证其行为。

更深入的技术说明(可直接略过)APIRouter 并不是被 "mount"(挂载)的,它们与应用的其他部分并不隔离——因为 FastAPI 希望把它们的路径操作包含进 OpenAPI schema 与交互式界面中。FastAPI 保持原 Router 与路径操作持续活跃,并在处理请求与生成 OpenAPI 时合并 Router 的前缀、依赖、标签、响应及其他元数据。

通过 pyproject.toml 配置 fastapi 命令入口点

由于 FastAPI 应用对象位于 app/main.py,官方推荐在 pyproject.toml 中声明入口点:

[tool.fastapi]
entrypoint = "app.main:app"

它等价于 Python 代码:

from app.main import app

配置之后,fastapi 命令(uv run fastapi dev 等)就知道该去哪个模块找应用。虽然也可以每次在命令行显式传路径:

$ uv run fastapi dev app/main.py

但那样每次调用 fastapi 命令都要记住传对路径,而且其他工具可能无法据此定位应用(例如 VS Code 的 FastAPI 扩展等编辑器工具)。因此推荐始终使用 pyproject.toml 中的 entrypoint

运行并检查自动生成的 API 文档

在项目根目录运行:

$ uv run fastapi dev
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

然后打开浏览器访问 http://127.0.0.1:8000/docs。你会看到自动生成的交互式 API 文档,所有子模块的路径都已按正确路径(含前缀)与正确标签归组展示。本教程示例的文档界面截图如下:

FastAPI 自动交互文档:users、items、custom、admin、default 各标签分组下的路由列表

从截图与 tests/test_tutorial/test_bigger_applications/test_main.py 中的 test_openapi_schema 快照可以核对最终的 OpenAPI 结果:

  • users 标签下:GET /users/GET /users/meGET /users/{username}
  • items 标签下:GET /items/GET /items/{item_id}PUT /items/{item_id}(PUT 额外携带 custom 标签,所以同一路径在 custom 分组中再次出现),且都声明了 404 额外响应;
  • admin 标签下:POST /admin/,声明了 418 额外响应,并同时要求 token 查询参数与 x-token 请求头;
  • default(默认组)下:GET / 根路由。

同时注意:每个路径操作都带上了全局依赖产生的 token 查询参数,而 items/admin 相关路径额外带上了 x-token 头参数——这正是"应用级 + Router 级"依赖叠加在 OpenAPI schema 中的直接体现。

进阶技巧

同一 Router 以不同前缀多次 include

可以对同一个 Router 调用多次 .include_router(),并使用不同前缀。例如希望同一套 API 同时暴露在 /api/v1/api/latest 两个前缀下时非常有用:

app.include_router(router, prefix="/api/v1")
app.include_router(router, prefix="/api/latest")

这是相对进阶的用法,多数场景未必需要,但 FastAPI 提供了这样的灵活性。

在另一个 APIRouter 中 include APIRouter

就像把 APIRouter 并入 FastAPI 应用一样,你也可以把 APIRouter 并入另一个 APIRouter

router.include_router(other_router)

这件事可以在把 router 并入 FastAPI 应用之前或之后完成,other_router 中的路径操作都会出现在路由表与 OpenAPI 中。同理,之后才添加的路径操作同样可以通过早先的 include 关系对主应用可见。

警告:不要直接改写 router.routes

官方文档对此给出明确警告:Router 被 include 之后,避免直接修改 router.routes。因为 FastAPI 将 router 的包含关系视为"活"的——原始 Router 及其路由始终参与路由与 OpenAPI 生成。应使用文档化的公开 API 来增补路由,例如路径操作装饰器(@router.get(...) 等)与 .include_router()

应当把 router.routes 当作一棵低层路由树:它既可以容纳路由定义,也可能容纳被 include 进来的 Router 条目,而不要把它当作"最终路径操作的扁平列表"来依赖。

小结

把 FastAPI 应用拆成多文件的核心套路可以总结为四条:

  1. 目录即包:给 appapp/routersapp/internal 等目录都放上 __init__.py,形成清晰的 Python 命名空间,供模块互相导入;
  2. 按域拆 Router:每个业务域(users、items、admin……)一个文件,用 router = APIRouter(...) 集中声明该域共享的 prefixtagsresponsesdependencies
  3. 主文件只做装配app/main.py 创建 FastAPI 实例(可加全局依赖),再用 app.include_router() 把各子模块 Router 合入,并可在 include 时对第三方共享 Router 附加前缀与安全依赖而不改动其源码;
  4. 入口点显式化:在 pyproject.toml[tool.fastapi] 下配置 entrypoint,让 fastapi 命令与编辑器工具都能准确找到应用对象。

示例源码完整存放在 docs_src/bigger_applications/app_an_py310/,配套测试覆盖了依赖顺序、前缀、标签、各状态码与 OpenAPI schema 快照(tests/test_tutorial/test_bigger_applications/test_main.py),可作为你搭建多模块 FastAPI 工程时直接对照的样板。

登录后查看全文
热门项目推荐
相关项目推荐