FastAPI 大型应用多文件架构:用 APIRouter 与包结构拆分项目实战

FastAPI 大型应用多文件架构:用 APIRouter 与包结构拆分项目实战 FastAPI 大型应用多文件架构用 APIRouter 与包结构拆分项目实战【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi导读任何真实业务的后端 API 都不会只活在一个文件里随着路由、依赖与业务逻辑的增长把代码拆进多个 Python 包与模块、再按模块组织路由是 FastAPI 工程化的第一步。本文以 docs/fr/docs/tutorial/bigger-applications.md英文版见 docs/en/docs/tutorial/bigger-applications.md为主线完整演示一套可复制的app/目录结构用APIRouter声明路径操作、把共享依赖抽成独立模块、在主应用中用include_router()组合所有路由并在pyproject.toml中配置 CLI 入口。读完你将掌握从单文件快速成长为多模块、可扩展、易协作项目所需的关键模式与底层原理。何时需要更大的应用它与 Flask Blueprint 的关系当你构建一个稍具规模的应用或 Web API 时几乎不可能把所有内容放进单个文件。FastAPI为此提供了便捷工具在保留完整灵活性的前提下把应用结构化为多个文件。如果你从 Flask 迁移而来可以将这里的做法类比为 Flask 的Blueprints一套按业务域拆分的可插拔路由集合最终统一挂载到主应用上。FastAPI 中没有独立的蓝图对象承担这一角色的是APIRouter从源码看它定义于 routing.py配合 Python 的包/模块机制共同完成分层。示例文件结构让目录布局表达你的代码分层教程假设你规划出如下文件结构. ├── app │ ├── __init__.py │ ├── main.py │ ├── dependencies.py │ └── routers │ │ ├── __init__.py │ │ ├── items.py │ │ └── users.py │ └── internal │ ├── __init__.py │ └── admin.py每个目录与子目录中都放置了一个__init__.py——正是这个可为空的文件让 Python 把目录识别为包package从而允许代码在文件之间相互导入。例如在app/main.py中就可以写from app.routers import items。从 Python 语义上理解这一布局这正是后续相对导入的基础app/含app/__init__.py整体是一个Python 包appapp/main.py位于包内是它的模块app.mainapp/dependencies.py同样是模块app.dependenciesapp/routers/内含另一个__init__.py构成子包app.routersapp/routers/items.py与app/routers/users.py分别是子模块app.routers.items、app.routers.usersapp/internal/是又一个子包app.internalapp/internal/admin.py是对应的子模块app.internal.admin。下文示意图直观展示了目录与包、模块的映射关系该图在本仓库中的位置为 package.drawio.svg。如果为每个文件补上注释目录本身就成了一份架构说明书. ├── 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应用的一部分同一个 Python 包内。为此可以在该模块中用APIRouter来创建路径操作。导入并实例化APIRouter像使用FastAPI类一样导入并创建实例from fastapi import APIRouter router APIRouter()对应源码文件为 app_an_py310/routers/users.py。声明路径操作随后像使用FastAPI类那样声明路径操作router.get(...)等装饰器用法与app.get(...)完全一致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}你可以把APIRouter理解为一个迷你FastAPIFastAPI支持的全部选项在这里同样支持——同样的parameters、responses、dependencies、tags等等。从 routing.py 的源码结构看APIRouter本身就是复用 FastAPI 路由核心逻辑的类。示例中的变量名叫router但可以随意命名。现在先不急着把这个APIRouter挂进主应用——先看依赖再看另一个APIRouter。把跨模块共享的依赖抽到dependencies.py许多路径操作都需要用到某些依赖。为避免重复定义教程将它们放入独立的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_code400, detailX-Token header invalid) async def get_query_token(token: str): if token ! jessica: raise HTTPException(status_code400, detailNo Jessica token provided)对应文件为 app_an_py310/dependencies.py。两个依赖的意图都很直白get_token_header从X-Token请求头取字符串与预期值不符时抛出HTTPException(400)get_query_token校验查询参数token它稍后会被注册为整个应用的全局依赖。这里用了一个虚构的请求头来简化示例真实项目中应优先使用 FastAPI 内置的 OAuth2、API Key、HTTP Basic/Bearer 等安全工具见本教程系列中的 Security 相关章节而不是自己发明认证头。创建带公共prefix、tags、responses、dependencies的 items 路由模块假定应用里还有一个专门处理 items 的模块app/routers/items.py其中有两类路径操作/items//items/{item_id}结构与users.py相同但本模块内的所有路径操作共享一批公共属性路径前缀/items、标签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_code404, detailItem 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_code403, detailYou can only update the item: plumbus ) return {item_id: item_id, name: The great Plumbus}对应文件为 app_an_py310/routers/items.py。关键规则prefix不能以/结尾每个路径操作本身必须以/开头例如router.get(/{item_id}) async def read_item(item_id: str): ...因此prefix不能再包含结尾的/。本模块的prefix是/items而不是/items/最终生成的路径为/items/与/items/{item_id}与预期一致。继承到每个路径操作的公共元数据在APIRouter上声明的这些配置会应用到该路由器的所有路径操作上tags[items]这些标签对基于 OpenAPI 的自动交互文档/docs尤其有用会把同组路径归类展示responses{404: ...}所有路径操作的 OpenAPI 定义中都会包含预定义的404响应dependencies[Depends(get_token_header)]列表中的依赖会对本路由器每个请求先于路径函数执行并解析。注意这里的路由级dependencies与路径操作装饰器上的 dependencies类似它们的返回值不会被传入你的路径操作函数。若某个具体路径操作又额外声明了依赖则两者都会执行。执行顺序是先执行路由器级依赖再执行装饰器级dependencies最后才解析普通函数参数依赖如item_id、x_token这类参数。此外这里也可以加入带scopes的Security依赖。利用路由器级dependencies的典型场景是为一整组路径操作统一要求认证而无需逐条添加。prefix、tags、responses、dependencies这些参数与 FastAPI 中许多其他场景一样本质上是框架提供的避免代码重复的便捷能力。在单个路径操作上叠加更多 tags 与 responses不把/items前缀和tags[items]写进每个路径操作是因为它们已在APIRouter上声明。但我们仍可为某个具体路径操作追加额外tags与专属responses——如update_item装饰器所示它声明了tags[custom]和responses{403: ...}。最终该路径操作的标签是两者的合并结果[items, custom]文档中同时呈现两条附加响应路由器的404与自身的403。用..相对导入获取依赖items.py位于app.routers.items而依赖函数在app.dependencies即app/dependencies.py于是使用带两个点的相对导入from ..dependencies import get_token_header理解相对导入的点数若你对 Python 相对导入并不熟悉下面做一次完整推演以app/routers/items.py为出发点单个点.如from .dependencies import get_token_header表示从当前模块所在包app/routers/出发寻找该包下的dependencies模块——即虚构的app/routers/dependencies.py。但该文件并不存在我们的依赖在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已是顶层包没有更上级的父包因此会抛出导入错误。理解点数与包层级的关系后无论结构多复杂你都能正确书写相对导入。组装主应用main.pyFastAPIinclude_router主模块app/main.py负责把所有部件衔接起来。因为大部分逻辑已经迁入各自模块主文件保持得相当精简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: Im a teapot}}, ) app.get(/) async def root(): return {message: Hello Bigger Applications!}对应文件为 app_an_py310/main.py。逐层拆解这段代码1. 声明全局依赖像往常一样导入并创建FastAPI实例同时可在FastAPI(...)上声明全局依赖dependencies[Depends(get_query_token)]。该依赖会与每个APIRouter自身的依赖组合生效items.router的get_token_header照常执行全局的get_query_token也会在每个请求上先被执行。2. 相对导入各子模块users.py、items.py同属app包可用单点相对导入from .routers import items, users其含义是从main.py所在包app/出发找到子包routersapp/routers/从中导入子模块items与users。模块items内有一个router变量items.router即先前创建的那个APIRouter对象users同理。也可以写等价的全限定绝对导入from app.routers import items, users——两种方式仅风格不同。3. 刻意导入模块而非对象避免名称冲突为什么导入子模块items而不直接from .routers.items import router因为users子模块里也恰好有一个名为router的变量from .routers.items import router from .routers.users import router若顺序导入后者的router会覆盖前者两个对象就无法同时使用。因此这里导入模块本身再通过items.router、users.router分别访问既消除了命名冲突也保留了清晰的命名空间。4. 用include_router()把路由并入主应用app.include_router(users.router)与app.include_router(items.router)会把对应路由器里的全部路由并入应用。users.router是app/routers/users.py中的APIRouteritems.router同理。值得注意的实现细节FastAPI 在把路由器包含进主应用时会保持原APIRouter及其APIRoute处于激活状态主应用侧的 FastAPI.include_router 在合并各路由器的同时保留原引用这意味着自定义的APIRouter/APIRoute子类在路由器被包含后仍能参与处理流程。并且包含路由器是为轻量而设计的不会给每个请求引入额外开销无需担心性能。5. 不改动原路由器用include_router参数叠加prefix/tags/responses/dependencies设想app/internal/admin.py由组织内多个项目共享内含一些管理后台路径操作因此不能为了本项目需求直接修改它去添加prefix、dependencies、tags。它的源码非常简洁from fastapi import APIRouter router APIRouter() router.post(/) async def update_admin(): return {message: Admin getting schwifty}对应文件为 app_an_py310/internal/admin.py。解决方案是把这些参数传给include_router()在主应用侧完成二次定制app.include_router( admin.router, prefix/admin, tags[admin], dependencies[Depends(get_token_header)], responses{418: {description: Im a teapot}}, )效果在本应用中admin模块的每个路径操作都获得前缀/admin、标签admin、依赖get_token_header与418响应而原始admin.router保持未改动其他项目可继续复用它并采用完全不同的认证方式。prefix、tags、dependencies、responses在主应用与路由器两个层面都可配置覆盖关系由include_router的参数决定——这也是避免在多项目间复制粘贴路由定义的关键手段。6. 直接在主应用上声明路径操作依然可以老式地直接在FastAPI应用上添加路径操作这里仅演示可行性app.get(/) async def root(): return {message: Hello Bigger Applications!}它与所有通过include_router()添加的路径操作共存并正常工作。深入路由器并非挂载而是元数据合并这是一个你可能可以直接跳过的极技术性细节但它能解释许多行为APIRouter并不会被挂载(mount)也不会与应用其他部分隔离。因为 FastAPI 需要把它们的路径操作纳入 OpenAPI schema 与交互文档实现方式是保持原始路由器和路径操作激活在处理请求与生成 OpenAPI 时动态合并路由器的前缀、依赖、标签、响应及其他元数据。路由与路由器的组织在 routing.py 的APIRouter.include_router见 routing.py中完成——前缀、依赖等配置在构建路由时逐级拼接。配置pyproject.toml入口并启动应用既然app对象位于app/main.py可在你的应用项目的pyproject.toml中配置 CLI 入口[tool.fastapi] entrypoint app.main:app它等价于 Python 代码from app.main import app。配置后fastapi命令就会知道去哪里寻找你的应用。也可以不写配置、每次显式传路径启动$ uv run fastapi dev app/main.py但这样每次调用fastapi命令都得记住传入正确路径而且 VS Code 扩展等其他工具可能无法据此定位应用因此官方推荐优先使用pyproject.toml中的entrypoint。运行并检查自动生成的 API 文档完成以上步骤后启动应用$ uv run fastapi dev INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRLC to quit)打开 http://127.0.0.1:8000/docs你会看到自动生成的交互式 API 文档来自所有子模块的路径都被列出且带上了正确的前缀与标签分组。实际运行效果截图见 image01.png。本仓库中还有对应的自动化测试 tests/test_tutorial/test_bigger_applications/test_main.py验证的就是docs_src/bigger_applications/app_an_py310这套示例在真实请求下的路由、依赖与响应行为可作为你自测的参照。进阶技巧路由复用与路由器嵌套同一路由器用不同prefix多次包含include_router()可以针对同一个路由器调用多次并使用不同前缀。一个实用场景是把同一套 API 暴露到多个版本前缀下例如同时提供/api/v1与/api/latestapp.include_router(router, prefix/api/v1) app.include_router(router, prefix/api/latest)这属于进阶用法多数场景未必需要但框架已原生支持。把一个APIRouter包含进另一个APIRouter与把路由器包含进FastAPI应用的方式类似也可以把路由器嵌进另一路由器router.include_router(other_router)该操作可以在router被并入FastAPI应用之前或之后进行other_router的路径操作都会被纳入路由匹配与 OpenAPI。同样的规则适用于后续新添加的路径操作——它们也会通过早先的包含关系自动可见因为路由器的包含是实时生效的。警告不要在包含后直接修改router.routes路由器被包含后应避免直接修改router.routes。FastAPI 将路由器的包含视为live动态生效原始路由器及其路由始终参与路由与 OpenAPI 生成。请使用文档化的 API——路径操作装饰器与.include_router()——来增删路由与子路由器应把router.routes视作一个可能同时包含路由定义与已包含子路由器的低层路由树而不要依赖它作为最终路径操作的扁平列表。小结多文件化是 FastAPI 项目走向清晰与可维护的关键一步核心要点可归纳为目录 包文件 模块通过__init__.py建立包/子包层次用./../...相对导入表达同级包、父包、祖父包的关系APIRouter 模块级的路由容器像使用FastAPI一样声明路径操作并支持prefix、tags、responses、dependencies等公共配置避免逐条重复include_router() 装配接口主应用导入各模块对象而非同名变量用include_router()合并全部路由对第三方共享的admin.router可在包含时叠加prefix、tags、dependencies、responses而无需改动其源码依赖分层执行全局依赖FastAPI级→ 路由器级依赖 → 路径装饰器dependencies→ 参数依赖返回值不会注入路径函数启动与自测在pyproject.toml配置[tool.fastapi] entrypoint后用uv run fastapi dev启动配合自动文档与仓库内测试校验行为。这套模式让你的路由、依赖与业务逻辑各得其所也天然支持同一套路由在多项目、多前缀间复用。【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考