FastAPI 大型应用组织实战:多文件结构、APIRouter 模块化拆分与统一装配
FastAPI 允许(甚至鼓励)把接口逻辑分散到多个 Python 模块中,再通过 APIRouter 与 app.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 类上支持的 parameters、responses、dependencies、tags 等选项,它全部支持。
推荐目录结构:一个 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
[](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.items,app/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.py、dependencies.py、routers/{users,items}.py、internal/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.py。APIRouter 的类实现在 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 工具(安全性依赖)(如 OAuth2PasswordBearer、HTTPBearer、APIKeyHeader 等),它们会提供更完善的 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 级依赖列表会在每个请求到达时被求解/执行。如果某个具体路径操作自己也声明了依赖,两者都会执行。
依赖的执行顺序
教程明确给出的执行次序是:
- 先执行
APIRouter上声明的依赖; - 再执行路径操作装饰器中的
dependencies(路径操作装饰器依赖); - 最后执行普通参数依赖(即通过函数参数 +
Depends()注入的依赖)。
另外,你还可以在此基础上叠加带 scopes 的 Security 依赖(即 OAuth2 作用域)用于细粒度授权。一个典型用途:把"认证"做成 APIRouter 级依赖,从而一次性保护整组路径操作,即使它们各自没有单独声明。prefix、tags、responses、dependencies 这些参数本质上都是 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_put、test_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.py、items.py 与 main.py 同属 Python 包 app,可以用单点相对导入:
from .routers import items, users
含义:从当前模块(app/main.py)所在包(目录 app/)出发,找子包 routers(目录 app/routers/),从中导入子模块 items(app/routers/items.py)与 users(app/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.router、users.router 访问各自的路由器对象,避免名称冲突。
用 include_router 加入 users 与 items
users.router 封装的是 app/routers/users.py 内的 APIRouter,items.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(示例里只包含一个极简的路径操作),我们不能修改它来加 prefix、dependencies、tags 等:
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.py:prefix(可选路径前缀,默认 "")、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-Token 时 POST /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 文档,所有子模块的路径都已按正确路径(含前缀)与正确标签归组展示。本教程示例的文档界面截图如下:
从截图与 tests/test_tutorial/test_bigger_applications/test_main.py 中的 test_openapi_schema 快照可以核对最终的 OpenAPI 结果:
users标签下:GET /users/、GET /users/me、GET /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 应用拆成多文件的核心套路可以总结为四条:
- 目录即包:给
app、app/routers、app/internal等目录都放上__init__.py,形成清晰的 Python 命名空间,供模块互相导入; - 按域拆 Router:每个业务域(users、items、admin……)一个文件,用
router = APIRouter(...)集中声明该域共享的prefix、tags、responses、dependencies; - 主文件只做装配:
app/main.py创建FastAPI实例(可加全局依赖),再用app.include_router()把各子模块 Router 合入,并可在 include 时对第三方共享 Router 附加前缀与安全依赖而不改动其源码; - 入口点显式化:在
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 工程时直接对照的样板。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
