FastAPI 大型应用拆分:使用 APIRouter 与多文件结构组织项目
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
导读
当 FastAPI 应用从"一个文件跑通"逐步成长为企业级项目时,把所有路由、依赖和业务逻辑堆在单个main.py里会迅速变得难以维护。本篇指南以 FastAPI 官方文档《Aplicações Maiores - Múltiplos Arquivos》为骨架,讲解如何用 Python 包(Package)结构 +APIRouter将应用拆分为多个模块:包括目录布局、路由注册、依赖抽取、相对导入、prefix/tags/responses批量配置,以及pyproject.toml入口配置与调试运行。读完你将能独立搭建一个结构清晰、易于扩展和团队协作的多文件 FastAPI 项目。
为什么需要多文件结构
任何真实的应用或 Web API,几乎不可能把全部代码塞进一个文件里。FastAPI提供了APIRouter这一便捷工具,让你可以在保持全部灵活性的前提下,把应用组织成多个模块。如果你熟悉 Flask,可以把APIRouter理解为 Flask 的 Blueprints(蓝图)——两者都是"把一个应用按业务领域切分成独立模块,再统一挂载"的机制。
拆分带来的直接收益:
- 每个业务域(用户、物品、管理员……)有独立的文件,职责单一;
- 路由操作与依赖、工具函数分层放置,便于复用;
- 团队可并行开发不同模块而少冲突;
- 自动生成的 OpenAPI 文档按
tags分组,接口更清晰。
示例文件结构
假设你计划采用如下目录布局:
. ├── app │ ├── __init__.py │ ├── main.py │ ├── dependencies.py │ └── routers │ │ ├── __init__.py │ │ ├── items.py │ │ └── users.py │ └── internal │ ├── __init__.py │ └── admin.py每个目录(或子目录)里都有一个__init__.py文件——正是这些文件让目录成为"Python 包",从而可以在文件之间相互导入代码。例如在app/main.py中就可以写:
from app.routers import items这套结构中各文件的"身份"如下:
- 目录
app包含全部内容,空文件app/__init__.py使其成为 Python 包:app; app/main.py位于包内,是包的一个模块:app.main;app/dependencies.py同样是模块:app.dependencies;- 子目录
app/routers/有自己的__init__.py,构成子包: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。
带注释的完整结构如下:
. ├── app # "app" 是一个 Python 包 │ ├── __init__.py # 该文件使 "app" 成为 "Python 包" │ ├── main.py # "main" 模块,例如 import app.main │ ├── 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说明:上述结构在本文对应仓库中已有完整可运行的参考实现,位于 docs_src/bigger_applications/app_an_py310/,下文所有代码均取自该目录。
用 APIRouter 拆分用户模块
假设专门处理用户的文件是子模块app/routers/users.py。你希望把与用户相关的路由操作与其余代码隔离,但让它仍然是同一个 FastAPI Web API(同一个 Python 包)的一部分。此时就可以用APIRouter来组织这些路由操作。
导入并实例化 APIRouter
导入方式与创建FastAPI类实例完全一致:
from fastapi import APIRouter router = APIRouter()完整代码见 docs_src/bigger_applications/app_an_py310/routers/users.py。
声明路由操作
接下来用它与使用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}对应源码见 users.py。
你可以把APIRouter想象成一个"迷你FastAPI":FastAPI类支持的所有选项它都支持——同样的parameters、responses、dependencies、tags等。示例中变量名用了router,你可以随意命名,比如api、user_router。
先不急着把该APIRouter挂到主应用,我们还需要检查一下依赖和另一个APIRouter。
抽取共享依赖
应用中多处都会用到一些依赖,因此把它们放进独立的dependencies模块(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。
提示:示例使用了虚构的请求头以简化演示。真实项目中,应优先使用 FastAPI 内置的安全工具(如
OAuth2PasswordBearer、HTTPBearer等)来获得更可靠、更规范的认证方案。
另一个带 APIRouter 的模块:批量配置 prefix、tags、responses、dependencies
假设app/routers/items.py模块负责处理"物品(items)",包含两条路由操作:
/items//items/{item_id}
结构上与users.py相同,但我们可以更聪明一点,让代码更简洁。既然该模块所有路由操作都共享同样的特征——路径前缀/items、标签tags: ["items"]、额外的responses、以及都需要的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}源码见 docs_src/bigger_applications/app_an_py310/routers/items.py。
前缀不能以 / 结尾
由于每条路由操作的路径必须以/开头,例如:
@router.get("/{item_id}") async def read_item(item_id: str): ...所以prefix不应包含末尾的/,这里的正确写法是/items。
各参数的生效范围
tags:一个字符串列表,会应用到该 router 下所有路由操作,尤其对自动交互文档(基于 OpenAPI)的接口分组非常有用;responses:预定义的额外响应(如404)会出现在所有路由操作的 OpenAPI 定义中;dependencies:一个Depends()列表,在 router 中声明后,会对发往该 router 下每条路由操作的每一个请求执行/解析这些依赖。
提示:与路径操作装饰器上的依赖一样,router 级依赖的返回值不会被传给路由操作函数。
最终效果是 items 的路径变成:
/items//items/{item_id}
并且:全部被打上包含单个字符串"items"的标签;全部包含预定义的responses;每条路由操作执行前都会先求值这些dependencies。
关于依赖执行顺序,从源码结构与依赖求解机制(fastapi/dependencies/utils.py)可以确认:如果你在具体路由操作上也声明了依赖,它们同样会被执行;执行顺序为——先执行 router 级依赖,再执行装饰器上的dependencies,最后执行普通参数依赖。此外,你还可以在依赖中叠加带scopes的Security依赖。
在实际工程中,把dependencies配在APIRouter上的典型用途是:为整组路由操作统一要求认证,而无需逐条添加。需要强调的是,prefix、tags、responses、dependencies这些参数本质上都是 FastAPI 帮助避免代码重复的便捷特性。
理解相对导入
上面的 items 代码位于模块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/,再上溯到app/的父包——但app已经是最顶层,没有父包了,因此会引用某个位于app/之上、带自己__init__.py的包。我们的示例中不存在这样的包,所以会直接报错。搞清楚.、..、...的规律后,无论应用多复杂,你都能正确使用相对导入。
为单条路由操作追加自定义 tags 与 responses
既然/items前缀和tags=["items"]已经配置在APIRouter上,就不需要再逐条重复。但我们仍然可以针对某条具体的路由操作追加额外的标签和专属的响应定义:
@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"}源码见 items.py。
提示:这条最后的路由操作在文档中会同时拥有
["items", "custom"]两组标签,并且会展示404与403两种响应定义。
主应用 main.py:把所有模块拼装起来
app/main.py是应用的入口文件,负责导入并实例化FastAPI类,把所有模块串起来。由于大部分业务逻辑已分散到各自的模块中,主文件会非常简洁。
导入 FastAPI 并声明全局依赖
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)])这里还声明了全局依赖,它会与每个APIRouter的依赖合并生效——即get_query_token会对应用中所有路由操作生效。
导入各 APIRouter 子模块
由于app/routers/users.py和app/routers/items.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)。模块items中有一个变量router(即items.router),就是我们之前在app/routers/items.py里创建的APIRouter实例;users模块同理。
也可以写成"绝对导入":
from app.routers import items, users两种写法等价;要深入了解 Python 包与模块机制,可参考 Python 官方文档《Modules 教程》。
避免命名冲突
这里直接导入子模块items,而不是只导入它的router变量,是因为users子模块中也有一个名为router的变量。如果写成:
from .routers.items import router from .routers.users import router后导入的users.router会覆盖前一个,二者就无法同时使用了。所以为了在同文件中同时使用两者,我们直接导入子模块,再通过items.router、users.router访问。
用 include_router 挂载路由
app.include_router(users.router) app.include_router(items.router)app.include_router()会把传入APIRouter的所有路由操作并入主应用,成为应用的一部分。其中users.router是app/routers/users.py内的APIRouter,items.router是app/routers/items.py内的APIRouter。
从实现上看,include_router是FastAPI应用类与APIRouter类共同提供的核心方法(见 fastapi/applications.py 与 fastapi/routing.py)。其签名支持prefix、tags、dependencies、default_response_class、responses、callbacks、deprecated、include_in_schema等参数,可以灵活控制被挂载路由的行为。
技术细节:FastAPI 在将 router 包含进主应用后,会保持原
APIRouter及其APIRoute处于激活状态。这意味着自定义的APIRouter/APIRoute子类在 router 被包含之后仍然可以参与路由处理。性能提示:不必担心包含 router 带来性能开销——该机制被设计得非常轻量,不会给每个请求增加额外负担,因此不会影响性能。
不改动原 router,用 include_router 附加 prefix/tags/responses/dependencies
现在假设组织给了你app/internal/admin.py文件,其中包含一个带若干管理员路由操作的APIRouter,这个文件要在多个项目间共享:
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。
由于它会被组织内其他项目复用,我们不能直接修改它去添加prefix、dependencies、tags等。但我们希望在本项目中:让它的所有路由操作以/admin开头、用已有的dependencies保护它、并附加tags和responses。这些都可以在app.include_router()时传入参数完成,无需改动原APIRouter:
app.include_router( admin.router, prefix="/admin", tags=["admin"], dependencies=[Depends(get_token_header)], responses={418: {"description": "I'm a teapot"}}, )这样原始APIRouter保持不变,app/internal/admin.py仍可与其他项目共享。在本项目中,admin 模块的每条路由操作最终都会获得:
- 前缀
/admin; - 标签
admin; - 依赖
get_token_header(每次请求先校验X-Token头); - 响应定义
418(I'm a teapot 🍵)。
这种覆盖只影响本项目中的应用,不影响任何其他使用该 router 的代码——例如,其他项目完全可以用不同的认证方式挂载同一个 router。
在主应用上直接添加路由操作
FastAPI应用同样可以直接添加路由操作。下面的例子只是为了展示这一能力:
@app.get("/") async def root(): return {"message": "Hello Bigger Applications!"}它与通过app.include_router()加入的路由操作可以完美共存、正常路由。
进阶技术细节(多数情况下可以跳过):
APIRouter并不是被"挂载(mounted)"的,它们与应用的其他部分并不隔离。这是因为我们希望把它们的路由操作纳入 OpenAPI schema 和交互式文档。FastAPI 保持 router 及其路由操作处于激活状态,在处理请求和生成 OpenAPI 时,会将 router 的 prefix、dependencies、tags、responses 等元数据合并生效。若想深入了解include_router的完整参数列表与默认值,可查看 fastapi/routing.py 中的 include_router 定义。
在 pyproject.toml 中配置入口点
由于 FastAPI 的app对象位于app/main.py,可以在pyproject.toml中配置入口点:
[tool.fastapi] entrypoint = "app.main:app"它等价于 Python 导入写法:
from app.main import app这样fastapi命令就知道去哪里寻找你的应用。也可以每次手动传路径:
$ uv run fastapi dev app/main.py但那样每次调用fastapi命令都要记得传对路径,而且其他工具可能找不到应用(例如 VS Code 扩展或 FastAPI Cloud)。因此推荐在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 文档:所有子模块的路径都已按正确的前缀和标签归组展示。
如上图所示:users分组下是/users/、/users/me、/users/{username};items分组下是/items/、/items/{item_id}(GET 与 PUT);custom分组下是带自定义标签的PUT /items/{item_id};admin分组下是POST /admin/;default分组下是GET /根路径——这正是prefix、tags、router 级配置协同生效的可视化结果。
进阶用法
同一 router 以不同 prefix 多次包含
可以多次调用.include_router()并传入不同的前缀来包含同一个router。典型场景是同一套 API 同时暴露在多个前缀下,例如/api/v1和/api/latest:
app.include_router(api_router, prefix="/api/v1") app.include_router(api_router, prefix="/api/latest")这属于进阶用法,多数应用未必需要,但需要时它就在那里。
在 APIRouter 中再包含 APIRouter
正如可以把APIRouter包含进FastAPI应用,也可以在另一个APIRouter中包含它:
router.include_router(other_router)这个操作在把router包含进FastAPI应用之前或之后进行都可以,FastAPI 都会把other_router的路由操作纳入路由与 OpenAPI。之后向 router 新增的路由操作同样会通过之前的包含关系可见。
⚠️ 技术警告:避免在包含 router 之后直接修改
router.routes。FastAPI 将 router 的包含视为"激活"状态,原 router 及其路由会持续参与路由与 OpenAPI 生成。请使用文档化的 API——如路由操作装饰器与.include_router()——来添加路由和 router。请把router.routes视为一棵低层路由树(可能包含路由定义与被包含的 router),不要依赖它作为最终路由操作的扁平列表。
小结与仓库对照
至此,你已经掌握了 FastAPI 大型应用拆分的完整套路:用 Python 包结构组织文件、用APIRouter切分业务域、抽取共享依赖、用相对导入串联模块、在include_router时批量附加prefix/tags/responses/dependencies,最后在pyproject.toml配置入口点并启动验证。
本文所有代码的完整可运行版本都在仓库的 docs_src/bigger_applications/app_an_py310/ 目录下,包含main.py、dependencies.py、routers/与internal/全部模块;include_router的底层实现可进一步查阅 fastapi/routing.py 与 fastapi/applications.py。建议直接对照源码阅读本文各节,边读边在自己的项目中实践,即可快速上手多文件架构的 FastAPI 开发。
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考