FastAPI 项目结构:打造模块化清晰的大型应用架构
推荐阅读 📚🎓🧑🏫
[1] 一起学Python 专栏:深入探讨 Python 编程,涵盖基础与进阶内容,以及 NumPy、Pandas、Matplotlib、Docker、Linux 等实用技术。
在构建大型应用时,保持清晰的目录结构是确保项目易于维护和扩展的关键。本文介绍了如何使用 FastAPI 组织应用代码,将不同功能模块(如路由、依赖、内部逻辑等)分开,形成模块化的项目架构。通过使用 APIRouter
组织路由,结合路径前缀、依赖注入和自定义响应,可以确保项目结构简洁明了,易于管理。此外,还提供了如何注册多个路由器、添加自定义前缀以及在不同版本之间暴露相同的接口的实践方法,帮助开发者构建高效、可扩展的 FastAPI 应用。
文章目录
预备课:Python Module 模块详解:模块导入与项目管理的最佳实践
以下示例中使用的 Python 版本为 Python 3.10.15
,FastAPI 版本为 0.115.4
。
一 示例代码
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!"}
在 your-package/
目录下,运行代码文件 main.py 来启动应用:
$ uvicorn app.main:app --reload
在 SwaggerUI 中可以查看在线文档:http://127.0.0.1:8000/docs
。
二 目录结构
your-package # 项目的根目录
├── app # 主应用目录,包含所有应用代码
│ ├── __init__.py # 将该目录标识为 Python 包,初始化包时执行的代码(如果需要)
│ ├── main.py # 应用的入口文件,通常包含 FastAPI 应用实例(app)和路由配置
│ ├── dependencies.py # 存放项目中常用的依赖,如数据库连接、认证、令牌处理等
│ └── routers # 存放所有路由(API 端点)的子目录
│ │ ├── __init__.py # 将 routers 目录标识为 Python 包,初始化包时执行的代码
│ │ ├── items.py # 处理与“物品”相关的 API 路由
│ │ └── users.py # 处理与“用户”相关的 API 路由
│ └── internal # 存放应用内部逻辑的目录(例如管理后台)
│ ├── __init__.py # 将 internal 目录标识为 Python 包,初始化包时执行的代码
│ └── admin.py # 管理员相关的功能,可能包括权限管理、后台管理等
your-package
:项目的根目录,包含所有应用文件。app
:主应用的代码目录,所有与业务相关的模块都会放在这里。__init__.py
:标识该目录为 Python 包,允许从该目录进行模块导入。在 Python 3.3+ 中虽然可以省略,但推荐在有多个子模块的目录中添加它,以保持包结构的清晰。main.py
:应用的启动文件,通常包含 FastAPI 的app
实例,并导入并绑定路由。dependencies.py
:存放与应用相关的共享依赖项,如数据库连接、认证机制等,供各模块引用。routers
:存放各个路由模块的目录,通常每个文件负责不同的功能模块(如用户管理、物品管理等)。internal
:存放应用内部管理相关功能的模块,例如管理员控制台或后台管理接口。
三 APIRouter
路由器
假设 /app/routers/users.py
用于处理用户逻辑,可以使用 APIRouter
将与用户相关的路径操作与其他代码分开,保持 FastAPI 应用的结构清晰。使用 APIRouter
声明 路径操作,方式与 FastAPI 类似。
# 导入 FastAPI 的 APIRouter 用于定义路由
from fastapi import APIRouter
# 创建一个新的 APIRouter 实例,用于将路由逻辑组织到一个模块中,router 变量名可以自定义。
router = APIRouter()
# 定义一个 GET 请求的路由,访问 "/users/" 路径时返回固定的用户列表
@router.get("/users/", tags=["users"])
async def read_users():
# 返回包含两个用户字典的列表
return [{"username": "Rick"}, {"username": "Morty"}]
# 定义一个 GET 请求的路由,访问 "/users/me" 路径时返回当前用户的假数据
@router.get("/users/me", tags=["users"])
async def read_user_me():
# 返回当前用户的用户名(假数据)
return {"username": "fakecurrentuser"}
# 定义一个 GET 请求的路由,访问 "/users/{username}" 路径时返回指定用户名的用户信息
@router.get("/users/{username}", tags=["users"])
async def read_user(username: str):
# 返回请求中提供的用户名
return {"username": username}
1 小技巧
将共同部分(如路径前缀 /items
、tags 为items
、额外的响应和依赖项)统一添加到 APIRouter
中,避免重复定义。在 app/routers/items.py
中,对于 URI 路径操作 /items/
和 /items/{item_id}
,可以将公共部分 /items
这样统一配置:
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}
2 依赖项的执行顺序
每个路径操作的路径必须以 /
开头,但前缀不能以 /
结尾,因此前缀为 /items
。可以为所有路径操作添加 tags
和额外的 responses
。同时,dependencies
列表将应用于所有路径操作,并在每个请求中执行。依赖项的执行顺序为:先执行路由器的依赖项,再执行装饰器中的依赖项,最后执行路径操作的依赖项。
3 相对导入
# 从当前目录的 dependencies 模块导入 get_token_header
from .dependencies import get_token_header
# 从上一级目录的 dependencies 模块导入 get_token_header
from ..dependencies import get_token_header
# 从上两级目录的 dependencies 模块导入 get_token_header
from ...dependencies import get_token_header
4 自定义 tags
、responses
和 dependencies
可以为特定路径操作添加额外的tags
、responses
和 dependencies
。
@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", "custom"]
,如下:
在文档中展示两个响应,一个用于 404,另一个用于 403,如下:
四 FastAPI 项目的 main.py
1 导入 FastAPI
from fastapi import Depends, FastAPI
from .dependencies import get_query_token, get_token_header
app = FastAPI(dependencies=[Depends(get_query_token)])
2 导入 APIRouter
相对导入
from .routers import items, users
也可以使用 绝对导入
from app.routers import items, users
3 避免名称冲突
如果像下面这样导入会产生冲突,如下:
from .routers.items import router
from .routers.users import router
导入子模块避免冲突,如下
from .routers import items, users
4 注册路由
# 将 'users' 模块中的路由注册到 FastAPI 应用中
app.include_router(users.router)
# 将 'items' 模块中的路由注册到 FastAPI 应用中
app.include_router(items.router)
app.include_router()
会为 APIRouter
中的每个路径操作自动创建相应的路径,因此所有路由像在同一应用中一样工作。注册路由仅在启动时花费几微秒,不会影响性能。
5 在注册路由时增加自定义参数
可以在不修改原始 APIRouter
的情况下添加定制化配置,且仅影响本应用中的 APIRouter
。
# 将 admin 路由器添加到 FastAPI 应用中,并配置自定义的参数
app.include_router(
admin.router, # 引入 admin 路由模块
prefix="/admin", # 为 admin 路由添加前缀 "/admin",所有路径都会以 "/admin" 开头
tags=["admin"], # 给 admin 路由添加标签 "admin",用于 OpenAPI 文档中分类
dependencies=[Depends(get_token_header)], # 为所有 admin 路由添加依赖项,所有请求都会先执行 get_token_header 函数
responses={418: {"description": "I'm a teapot"}}, # 为 admin 路由添加自定义响应码 418,并设置响应描述为 "I'm a teapot"
)
在 FastAPI 中,APIRouter
只是一个组织路径操作的工具,它并不会被完全隔离,而是通过 include_router()
方法将其路径操作加入到主应用中。所以,APIRouter
中的路径操作最终还是会和主应用紧密结合,能够共享文档、依赖项等,而不是像独立模块一样完全自给自足。FastAPI 会重新创建这些路径操作,使其能够在应用程序中正确注册并显示在 OpenAPI 文档中。
五 使用不同前缀暴露同一路由器
在某些场景下,一个 API 可能需要通过多个前缀来公开,如 /api/v1
和 /api/latest
。下面是一个实现示例:
# 定义路由器
router = APIRouter()
@router.get("/version")
async def get_items():
return {"version": ["version1", "version2", "version3"]}
# 通过不同的前缀包含相同的路由器
app.include_router(router, prefix="/api/v1")
app.include_router(router, prefix="/api/latest")
在这个示例中,/items
路径分别通过 /api/v1/items
和 /api/latest/items
两个端点对外暴露。通过这种方式,可以为相同的 API 设置不同的版本或前缀。这种做法有助于版本化管理,确保旧版 API 用户不受新版本改动的影响,保证系统的向后兼容性。如下图所示:
六 在 APIRouter 中包含另一个 APIRouter
# 定义第一个路由器
router_v1 = APIRouter()
@router_v1.get("/items1")
async def get_items_v1():
return {"items": ["item1", "item2", "item3"]}
# 定义第二个路由器
router_v2 = APIRouter()
@router_v2.get("/items2")
async def get_items_v2():
return {"items": ["itemA", "itemB", "itemC"]}
# 将 router_v2 包含到 router_v1 中
router_v1.include_router(router_v2, prefix="/v2")
# 包含 router_v1 到 FastAPI 应用中
app.include_router(router_v1, prefix="/api/v1")
可以在一个 APIRouter
中包含另一个 APIRouter
。在示例中,router_v1
包含了 router_v2
并为其设置了 /v2
前缀。访问 /api/v1/items
会返回版本 1 的数据,而访问 /api/v1/v2/items
会返回版本 2 的数据。为确保 router_v2
的路径操作被包含在 router_v1
中,需先通过 router_v1.include_router(router_v2)
将其包含进来,再将 router_v1
包含到 FastAPI 应用中。如下图所示:
七 完整代码示例
这是项目的 main.py ,全部示例见: GitHub your-package 。
from fastapi import Depends, FastAPI, APIRouter
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!"}
# 定义路由器
router = APIRouter()
@router.get("/version")
async def get_items():
return {"version": ["version1", "version2", "version3"]}
# 通过不同的前缀包含相同的路由器
app.include_router(router, prefix="/api/v1")
app.include_router(router, prefix="/api/latest")
# 定义第一个路由器
router_v1 = APIRouter()
@router_v1.get("/items1")
async def get_items_v1():
return {"items": ["item1", "item2", "item3"]}
# 定义第二个路由器
router_v2 = APIRouter()
@router_v2.get("/items2")
async def get_items_v2():
return {"items": ["itemA", "itemB", "itemC"]}
# 将 router_v2 包含到 router_v1 中
router_v1.include_router(router_v2, prefix="/v2")
# 包含 router_v1 到 FastAPI 应用中
app.include_router(router_v1, prefix="/api/v1")
八 源码地址
详情见: GitHub your-package
FastAPI其他源码见:GitHub FastApiProj
九 参考
[1] FastAPI 官方文档