文章目录
-
- [1. 为什么是 FastAPI:ASGI 时代的 Web 框架选型](#1. 为什么是 FastAPI:ASGI 时代的 Web 框架选型)
- [2. 项目分层与路由设计](#2. 项目分层与路由设计)
- [3. 路径参数与查询参数的类型安全](#3. 路径参数与查询参数的类型安全)
- [4. 请求体与 Pydantic 模型校验](#4. 请求体与 Pydantic 模型校验)
- [5. 响应模型:控制输出,隐藏敏感字段](#5. 响应模型:控制输出,隐藏敏感字段)
- [6. 依赖注入:FastAPI 的"灵魂特性"](#6. 依赖注入:FastAPI 的"灵魂特性")
-
- [6.1 Depends 的基本用法](#6.1 Depends 的基本用法)
- [6.2 子依赖与缓存](#6.2 子依赖与缓存)
- [6.3 依赖缓存的底层机制](#6.3 依赖缓存的底层机制)
- [6.4 可复用的依赖设计](#6.4 可复用的依赖设计)
- [7. 中间件与事件钩子](#7. 中间件与事件钩子)
-
- [7.1 HTTP 中间件](#7.1 HTTP 中间件)
- [7.2 Lifespan:替代 on_event 的新标准](#7.2 Lifespan:替代 on_event 的新标准)
- [8. 异常处理体系](#8. 异常处理体系)
-
- [8.1 HTTPException 的标准用法](#8.1 HTTPException 的标准用法)
- [8.2 自定义异常处理器](#8.2 自定义异常处理器)
- [9. 异步路由的正确姿势](#9. 异步路由的正确姿势)
-
- [9.1 async def vs def](#9.1 async def vs def)
- [9.2 什么时候不该用 async](#9.2 什么时候不该用 async)
- [10. 后台任务](#10. 后台任务)
- [11. OpenAPI 文档定制](#11. OpenAPI 文档定制)
- [12. 完整实战:图书管理 REST API](#12. 完整实战:图书管理 REST API)
- 总结
1. 为什么是 FastAPI:ASGI 时代的 Web 框架选型
Python Web 框架的演进大致经历了三个阶段:CGI 时代(每个请求启动一个新进程)、WSGI 时代(同步网关,如 Flask/Django)、ASGI 时代(异步网关,如 FastAPI/Starlette)。FastAPI 之所以在过去三年成为增长最快的 Python Web 框架,核心原因在于它将三件事合而为一:类型提示驱动的自动校验、自动生成 OpenAPI 文档、以及基于 Starlette 的高性能异步能力。
传统框架的典型痛点在于"重复声明"------路由函数里写一遍参数名,Pydantic 模型里写一遍字段定义,文档里再写一遍说明,校验逻辑还要额外写一段 if not isinstance(...)。三份代码描述同一件事,任何一处修改都要同步其他两处。FastAPI 的设计哲学是"声明一次,处处生效"------类型注解既是校验规则、也是文档来源、也是编辑器自动补全的依据。
下面这段对比直观展示了 FastAPI 和 Flask 在处理同一个"创建用户"接口时的代码量差异:
Flask 实现(需要手动校验和文档):
python
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/users", methods=["POST"])
def create_user():
data = request.get_json()
# 手动校验
if not isinstance(data.get("name"), str) or len(data["name"]) < 2:
return jsonify({"error": "name must be at least 2 chars"}), 422
if not isinstance(data.get("age"), int) or data["age"] < 0:
return jsonify({"error": "age must be non-negative"}), 422
user = {"id": 1, "name": data["name"], "age": data["age"]}
return jsonify(user), 201
FastAPI 实现(类型即校验):
python
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class UserCreate(BaseModel):
name: str = Field(min_length=2)
age: int = Field(ge=0)
@app.post("/users", status_code=201)
async def create_user(user: UserCreate):
return {"id": 1, "name": user.name, "age": user.age}
Flask 版本需要 12 行业务代码外加手动校验,FastAPI 版本仅需 10 行且自动获得输入校验、错误提示、OpenAPI 文档。更重要的是,当 UserCreate 模型增加一个 email 字段时,FastAPI 版本只需在模型中加一行 email: EmailStr,校验和文档自动生效,而 Flask 版本需要手动在路由函数中追加校验逻辑和文档说明。
#mermaid-svg-ZEw5psgoesdBDWGw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZEw5psgoesdBDWGw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZEw5psgoesdBDWGw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZEw5psgoesdBDWGw .error-icon{fill:#552222;}#mermaid-svg-ZEw5psgoesdBDWGw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZEw5psgoesdBDWGw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZEw5psgoesdBDWGw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZEw5psgoesdBDWGw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZEw5psgoesdBDWGw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZEw5psgoesdBDWGw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZEw5psgoesdBDWGw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZEw5psgoesdBDWGw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZEw5psgoesdBDWGw .marker.cross{stroke:#333333;}#mermaid-svg-ZEw5psgoesdBDWGw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZEw5psgoesdBDWGw p{margin:0;}#mermaid-svg-ZEw5psgoesdBDWGw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZEw5psgoesdBDWGw .cluster-label text{fill:#333;}#mermaid-svg-ZEw5psgoesdBDWGw .cluster-label span{color:#333;}#mermaid-svg-ZEw5psgoesdBDWGw .cluster-label span p{background-color:transparent;}#mermaid-svg-ZEw5psgoesdBDWGw .label text,#mermaid-svg-ZEw5psgoesdBDWGw span{fill:#333;color:#333;}#mermaid-svg-ZEw5psgoesdBDWGw .node rect,#mermaid-svg-ZEw5psgoesdBDWGw .node circle,#mermaid-svg-ZEw5psgoesdBDWGw .node ellipse,#mermaid-svg-ZEw5psgoesdBDWGw .node polygon,#mermaid-svg-ZEw5psgoesdBDWGw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZEw5psgoesdBDWGw .rough-node .label text,#mermaid-svg-ZEw5psgoesdBDWGw .node .label text,#mermaid-svg-ZEw5psgoesdBDWGw .image-shape .label,#mermaid-svg-ZEw5psgoesdBDWGw .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZEw5psgoesdBDWGw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZEw5psgoesdBDWGw .rough-node .label,#mermaid-svg-ZEw5psgoesdBDWGw .node .label,#mermaid-svg-ZEw5psgoesdBDWGw .image-shape .label,#mermaid-svg-ZEw5psgoesdBDWGw .icon-shape .label{text-align:center;}#mermaid-svg-ZEw5psgoesdBDWGw .node.clickable{cursor:pointer;}#mermaid-svg-ZEw5psgoesdBDWGw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZEw5psgoesdBDWGw .arrowheadPath{fill:#333333;}#mermaid-svg-ZEw5psgoesdBDWGw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZEw5psgoesdBDWGw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZEw5psgoesdBDWGw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZEw5psgoesdBDWGw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZEw5psgoesdBDWGw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZEw5psgoesdBDWGw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZEw5psgoesdBDWGw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZEw5psgoesdBDWGw .cluster text{fill:#333;}#mermaid-svg-ZEw5psgoesdBDWGw .cluster span{color:#333;}#mermaid-svg-ZEw5psgoesdBDWGw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZEw5psgoesdBDWGw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZEw5psgoesdBDWGw rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZEw5psgoesdBDWGw .icon-shape,#mermaid-svg-ZEw5psgoesdBDWGw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZEw5psgoesdBDWGw .icon-shape p,#mermaid-svg-ZEw5psgoesdBDWGw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZEw5psgoesdBDWGw .icon-shape .label rect,#mermaid-svg-ZEw5psgoesdBDWGw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZEw5psgoesdBDWGw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZEw5psgoesdBDWGw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZEw5psgoesdBDWGw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 校验失败
校验通过
客户端请求 POST /users
ASGI Server
uvicorn
Starlette 中间件栈
路由匹配
app.router
路径参数解析
依赖注入递归解析
solve_dependencies
Pydantic 请求体校验
返回 422 + 详细错误
路由函数执行
response_model 序列化
返回 JSON 响应
客户端收到响应
上图展示了一次完整的 FastAPI 请求生命周期。每一步都有明确的职责边界:Starlette 负责 HTTP 协议和中间件调度,FastAPI 负责路由和依赖注入,Pydantic 负责数据校验和序列化。
2. 项目分层与路由设计
生产环境的 FastAPI 项目不会把所有代码塞进一个 main.py。一个合理的分层结构如下:
book-api/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI 实例创建、中间件注册、路由挂载
│ ├── models/
│ │ └── book.py # Pydantic 请求/响应模型
│ ├── routes/
│ │ └── books.py # 路由定义(仅负责参数接收和响应返回)
│ ├── services/
│ │ └── book_service.py # 业务逻辑(路由层不直接操作数据库)
│ ├── dependencies.py # 可复用的依赖注入函数
│ └── exceptions.py # 自定义异常类
└── requirements.txt
分层的关键原则是:路由层只做参数接收和响应返回,业务逻辑下沉到 service 层,数据访问下沉到 repository 层。当一个路由函数超过 20 行时,通常意味着业务逻辑没有正确下沉。
APIRouter 是 FastAPI 的路由分组工具,允许将相关接口组织到一个文件中:
python
# app/routes/books.py
from fastapi import APIRouter
router = APIRouter(prefix="/api/v1/books", tags=["books"])
@router.get("/")
async def list_books():
return {"books": []}
@router.get("/{book_id}")
async def get_book(book_id: int):
return {"id": book_id}
@router.post("/")
async def create_book():
return {"id": 1}
然后在 main.py 中使用 app.include_router(router) 挂载。prefix 参数支持版本化(/api/v1/),这在 API 迭代时尤其重要------当需要不兼容变更时,新建一个 v2 路由文件即可,旧版本继续服务,给调用方留出迁移时间。
3. 路径参数与查询参数的类型安全
FastAPI 的路由参数有两种:路径参数(URL 的一部分)和查询参数(?key=value)。两者的声明方式是相同的------都在函数签名中声明,FastAPI 自动根据参数是否出现在路径模板中判断类型:
python
from fastapi import Query, Path
@router.get("/{book_id}")
async def get_book(
book_id: int = Path(ge=1, description="图书ID"),
include_reviews: bool = Query(False, description="是否包含书评"),
):
...
book_id: int出现在路径模板/{book_id}中,因此被识别为路径参数include_reviews: bool未出现在路径模板中,被识别为查询参数
Query 和 Path 提供的校验约束在请求到达路由函数之前就生效了。如果客户端传入 book_id=0(小于 ge=1 的约束),FastAPI 会在路由函数执行之前就返回 422 错误,不会让非法数据进入业务逻辑。这种"门卫模式"极大简化了代码------路由函数内部可以假设所有参数都是合法的。
对于需要复杂校验的场景(如"书名和作者至少提供一个"),可以使用 Pydantic 的 model_validator:
python
from pydantic import BaseModel, model_validator
class BookSearch(BaseModel):
title: str | None = None
author: str | None = None
category: str | None = None
page: int = Field(default=1, ge=1)
page_size: int = Field(default=20, ge=1, le=100)
@model_validator(mode="after")
def check_at_least_one_filter(self):
if not any([self.title, self.author, self.category]):
raise ValueError("至少提供一个搜索条件:title、author 或 category")
return self
@router.get("/search")
async def search_books(params: BookSearch = Query()):
...
注意这里使用了 Query() 将 Pydantic 模型声明为查询参数------FastAPI 会自动将 ?title=Python&page=1 展平并填充到 BookSearch 对象中。
4. 请求体与 Pydantic 模型校验
当接口需要接收 JSON 请求体时,定义一个 BaseModel 子类作为类型注解即可:
python
from pydantic import BaseModel, Field, field_validator
from datetime import date
class BookCreate(BaseModel):
title: str = Field(min_length=1, max_length=200)
author: str = Field(min_length=1, max_length=100)
isbn: str = Field(pattern=r"^\d{3}-\d-\d{4}-\d{4}-\d$")
published_date: date
price: float = Field(gt=0, le=9999.99)
tags: list[str] = Field(default_factory=list, max_length=10)
@field_validator("isbn")
@classmethod
def validate_isbn_checksum(cls, v: str) -> str:
"""校验 ISBN-10 的校验位"""
digits = [int(d) for d in v.replace("-", "")[:-1]]
checksum = sum((10 - i) * d for i, d in enumerate(digits)) % 11
expected = v[-1]
computed = "X" if checksum == 10 else str(checksum)
if expected != computed:
raise ValueError(f"ISBN 校验位错误:期望 {computed},实际 {expected}")
return v
Pydantic v2 的校验器分为两个层级:field_validator 在单个字段解析完成后运行(适合独立校验,如 ISBN 格式),model_validator 在所有字段解析完成后运行(适合跨字段校验,如"开始日期必须早于结束日期")。
嵌套模型也是常见的场景。比如图书的创建接口可能同时包含图书基本信息和分类信息:
python
class CategoryRef(BaseModel):
id: int
name: str
class BookCreate(BaseModel):
title: str
author: str
categories: list[CategoryRef] # 嵌套模型列表
# ...
FastAPI 会递归校验嵌套模型的每一个字段。如果 categories[0].id 传入了字符串而非整数,同样会返回 422 错误,且错误信息精确到 body.categories.0.id 的路径。
5. 响应模型:控制输出,隐藏敏感字段
response_model 是 FastAPI 最容易被忽视但工程价值极高的特性。它做三件事:
- 过滤输出字段 ------路由函数返回的字典里即使有
password_hash,只要response_model中没定义这个字段,就不会出现在响应里 - 类型转换------ORM 对象自动转换为 Pydantic 模型,日期转为 ISO 字符串,Decimal 转为 float
- 文档生成------OpenAPI schema 的 response 部分自动生成
python
class BookResponse(BaseModel):
id: int
title: str
author: str
published_date: date
price: float
model_config = {"from_attributes": True} # 允许从 ORM 对象构建
class BookDetailResponse(BookResponse):
isbn: str
tags: list[str]
# 注意:不包含内部字段如 created_at、updated_at
@router.get("/{book_id}", response_model=BookDetailResponse)
async def get_book(book_id: int):
book = await book_service.get_by_id(book_id)
return book # ORM 对象,FastAPI 自动序列化为 BookDetailResponse
model_config = {"from_attributes": True} 是 Pydantic v2 替代 v1 中 orm_mode = True 的配置项,允许从对象的 . 属性(而非字典的 [] 键)读取数据,这意味着可以直接将 SQLAlchemy ORM 对象传给路由函数并在返回时自动序列化。
response_model_exclude_unset 是另一个实用的参数。当 PATCH 接口只需要返回被修改的字段时:
python
@router.patch("/{book_id}", response_model=BookResponse)
async def update_book(
book_id: int,
update: BookUpdate, # 所有字段 Optional
):
book = await book_service.partial_update(book_id, update)
return book # 使用 response_model_exclude_unset=True 只返回非默认字段
6. 依赖注入:FastAPI 的"灵魂特性"
依赖注入是 FastAPI 的架构基石。它的工作方式可以用一句话概括:路由函数声明自己需要什么,FastAPI 负责创建并注入。
6.1 Depends 的基本用法
python
from fastapi import Depends
async def get_db_session():
"""创建数据库会话并在请求结束后关闭"""
session = AsyncSessionLocal()
try:
yield session
finally:
await session.close()
@router.get("/{book_id}")
async def get_book(
book_id: int,
db: AsyncSession = Depends(get_db_session),
):
result = await db.execute(select(Book).where(Book.id == book_id))
return result.scalar_one_or_none()
Depends(get_db_session) 做了三件事:
- 调用
get_db_session()获得生成器对象 - 通过
next()获取生成器的第一个yield值(即session),注入到db参数 - 请求处理完成后,调用生成器的
close()触发finally块,关闭数据库连接
6.2 子依赖与缓存
依赖之间可以嵌套,形成依赖链。更关键的是,同一个依赖在同一次请求中只会执行一次------这是通过依赖缓存实现的:
python
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(get_db_session),
) -> User:
"""从 Token 解析当前用户"""
payload = decode_token(token)
user = await db.get(User, payload["sub"])
return user
async def get_admin_user(
current_user: User = Depends(get_current_user),
) -> User:
"""要求当前用户是管理员"""
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="需要管理员权限")
return current_user
@router.get("/admin/dashboard")
async def admin_dashboard(
db: AsyncSession = Depends(get_db_session),
admin: User = Depends(get_admin_user),
):
# get_db_session 在 get_current_user 中已经被调用过了
# 这里再次声明 Depends(get_db_session),但 FastAPI 会返回缓存的 session
...
在上面的例子中,get_db_session 被声明了两次(一次在 get_current_user 中,一次在 admin_dashboard 中),但 FastAPI 的依赖解析器发现这是同一个可调用对象,会返回第一次创建的 session 实例。这个缓存机制保证了同一个请求中使用的数据库连接是同一个------避免在一个请求中创建多个数据库事务。
6.3 依赖缓存的底层机制
FastAPI 在每次请求到来时维护一个依赖缓存字典,键是依赖函数本身,值是该函数在当前请求中的执行结果。解析流程如下:
#mermaid-svg-YwDdyIqGuR5pz7to{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YwDdyIqGuR5pz7to .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YwDdyIqGuR5pz7to .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YwDdyIqGuR5pz7to .error-icon{fill:#552222;}#mermaid-svg-YwDdyIqGuR5pz7to .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YwDdyIqGuR5pz7to .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YwDdyIqGuR5pz7to .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YwDdyIqGuR5pz7to .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YwDdyIqGuR5pz7to .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YwDdyIqGuR5pz7to .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YwDdyIqGuR5pz7to .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YwDdyIqGuR5pz7to .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YwDdyIqGuR5pz7to .marker.cross{stroke:#333333;}#mermaid-svg-YwDdyIqGuR5pz7to svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YwDdyIqGuR5pz7to p{margin:0;}#mermaid-svg-YwDdyIqGuR5pz7to .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YwDdyIqGuR5pz7to .cluster-label text{fill:#333;}#mermaid-svg-YwDdyIqGuR5pz7to .cluster-label span{color:#333;}#mermaid-svg-YwDdyIqGuR5pz7to .cluster-label span p{background-color:transparent;}#mermaid-svg-YwDdyIqGuR5pz7to .label text,#mermaid-svg-YwDdyIqGuR5pz7to span{fill:#333;color:#333;}#mermaid-svg-YwDdyIqGuR5pz7to .node rect,#mermaid-svg-YwDdyIqGuR5pz7to .node circle,#mermaid-svg-YwDdyIqGuR5pz7to .node ellipse,#mermaid-svg-YwDdyIqGuR5pz7to .node polygon,#mermaid-svg-YwDdyIqGuR5pz7to .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YwDdyIqGuR5pz7to .rough-node .label text,#mermaid-svg-YwDdyIqGuR5pz7to .node .label text,#mermaid-svg-YwDdyIqGuR5pz7to .image-shape .label,#mermaid-svg-YwDdyIqGuR5pz7to .icon-shape .label{text-anchor:middle;}#mermaid-svg-YwDdyIqGuR5pz7to .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YwDdyIqGuR5pz7to .rough-node .label,#mermaid-svg-YwDdyIqGuR5pz7to .node .label,#mermaid-svg-YwDdyIqGuR5pz7to .image-shape .label,#mermaid-svg-YwDdyIqGuR5pz7to .icon-shape .label{text-align:center;}#mermaid-svg-YwDdyIqGuR5pz7to .node.clickable{cursor:pointer;}#mermaid-svg-YwDdyIqGuR5pz7to .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YwDdyIqGuR5pz7to .arrowheadPath{fill:#333333;}#mermaid-svg-YwDdyIqGuR5pz7to .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YwDdyIqGuR5pz7to .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YwDdyIqGuR5pz7to .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YwDdyIqGuR5pz7to .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YwDdyIqGuR5pz7to .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YwDdyIqGuR5pz7to .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YwDdyIqGuR5pz7to .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YwDdyIqGuR5pz7to .cluster text{fill:#333;}#mermaid-svg-YwDdyIqGuR5pz7to .cluster span{color:#333;}#mermaid-svg-YwDdyIqGuR5pz7to div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YwDdyIqGuR5pz7to .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YwDdyIqGuR5pz7to rect.text{fill:none;stroke-width:0;}#mermaid-svg-YwDdyIqGuR5pz7to .icon-shape,#mermaid-svg-YwDdyIqGuR5pz7to .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YwDdyIqGuR5pz7to .icon-shape p,#mermaid-svg-YwDdyIqGuR5pz7to .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YwDdyIqGuR5pz7to .icon-shape .label rect,#mermaid-svg-YwDdyIqGuR5pz7to .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YwDdyIqGuR5pz7to .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YwDdyIqGuR5pz7to .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YwDdyIqGuR5pz7to :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
是
否
是
否
收到请求
遍历路由函数的参数列表
参数有 Depends?
直接从请求中解析参数
该依赖在当前请求中
已被解析过?
返回缓存结果
不重复执行
执行依赖函数
依赖函数本身
有子依赖?
递归解析子依赖
同样遵循缓存规则
将结果存入缓存
将结果注入路由参数
路由函数执行
这个缓存的生命周期是单次请求 。请求结束后,如果依赖是生成器函数(使用了 yield),FastAPI 会执行 finally 代码块完成资源清理。这个特性使得数据库连接、文件句柄等资源的生命周期管理变得简洁且不会遗漏。
6.4 可复用的依赖设计
好的依赖设计遵循单一职责原则。以下是实际项目中常用的依赖模式:
python
# 分页依赖
async def pagination(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
) -> dict:
return {"offset": (page - 1) * page_size, "limit": page_size}
# 排序依赖
async def ordering(
sort_by: str = Query("id"),
sort_order: str = Query("asc", pattern="^(asc|desc)$"),
) -> dict:
return {"field": sort_by, "direction": sort_order}
@router.get("/")
async def list_books(
db: AsyncSession = Depends(get_db_session),
paging: dict = Depends(pagination),
order: dict = Depends(ordering),
):
query = (
select(Book)
.order_by(getattr(Book, order["field"]))
.offset(paging["offset"])
.limit(paging["limit"])
)
if order["direction"] == "desc":
query = query.order_by(getattr(Book, order["field"]).desc())
result = await db.execute(query)
return result.scalars().all()
pagination 和 ordering 两个依赖可以在任何需要分页和排序的接口中复用------写一次,用无数次。
7. 中间件与事件钩子
7.1 HTTP 中间件
FastAPI 的中间件遵循 Starlette 的 ASGI 中间件协议------这是一个纯异步的洋葱模型:
python
from fastapi import Request
import time
import uuid
@app.middleware("http")
async def add_request_id_and_timing(request: Request, call_next):
"""注入 trace_id 并记录请求耗时"""
request.state.trace_id = str(uuid.uuid4())[:8]
start = time.perf_counter()
response = await call_next(request)
elapsed = time.perf_counter() - start
response.headers["X-Trace-ID"] = request.state.trace_id
response.headers["X-Process-Time"] = f"{elapsed:.4f}s"
return response
中间件的执行顺序是"注册的逆序":最先注册的中间件在最外层,最后注册的中间件在最内层。如果需要 CORS 中间件最先处理所有请求(包括 OPTIONS 预检),应该将它放在所有自定义中间件之前注册。
7.2 Lifespan:替代 on_event 的新标准
从 FastAPI 0.93 开始,推荐使用 lifespan 上下文管理器替代 @app.on_event("startup") 和 @app.on_event("shutdown"):
python
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
# 启动逻辑:初始化数据库连接池、预热模型缓存
await init_db_pool()
await warm_cache()
yield # 此处开始接收请求
# 关闭逻辑:优雅关闭连接池、刷新缓冲区
await close_db_pool()
await flush_metrics()
app = FastAPI(lifespan=lifespan)
lifespan 相比 on_event 有两个优势:一是启动和关闭逻辑在同一个作用域内,变量可以共享;二是异常传播更清晰------如果启动阶段出错,异常会直接阻止服务启动,而 on_event 的错误不容易被察觉。
8. 异常处理体系
8.1 HTTPException 的标准用法
python
from fastapi import HTTPException
@router.get("/{book_id}")
async def get_book(book_id: int, db: AsyncSession = Depends(get_db_session)):
book = await db.get(Book, book_id)
if not book:
raise HTTPException(
status_code=404,
detail={"message": f"图书 #{book_id} 不存在", "code": "BOOK_NOT_FOUND"},
)
return book
8.2 自定义异常处理器
当项目需要对所有未捕获异常统一格式化时,注册自定义异常处理器:
python
from fastapi.responses import JSONResponse
from fastapi import Request
class BusinessException(Exception):
def __init__(self, message: str, code: str, status_code: int = 400):
self.message = message
self.code = code
self.status_code = status_code
@app.exception_handler(BusinessException)
async def business_exception_handler(request: Request, exc: BusinessException):
return JSONResponse(
status_code=exc.status_code,
content={
"detail": exc.message,
"code": exc.code,
"trace_id": getattr(request.state, "trace_id", None),
},
)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
"""兜底处理:所有未捕获的异常返回 500"""
return JSONResponse(
status_code=500,
content={
"detail": "内部服务器错误",
"code": "INTERNAL_ERROR",
"trace_id": getattr(request.state, "trace_id", None),
},
)
生产环境的异常处理器关键设计点:
- 统一错误格式 :所有错误响应包含
code(机器可读的错误码)、detail(人类可读的说明)、trace_id(用于日志关联) - 不泄露内部信息 :全局异常处理器返回的
detail是固定的"内部服务器错误",不暴露具体的异常信息(如数据库连接字符串、堆栈跟踪) - 业务异常分类 :定义
BusinessException子类体系(ValidationException、ResourceNotFoundException、PermissionDeniedException),每个子类自带 HTTP 状态码
9. 异步路由的正确姿势
9.1 async def vs def
FastAPI 支持两种路由函数定义方式:
python
# 同步路由:在线程池中执行
@router.get("/sync")
def sync_endpoint():
time.sleep(1) # 阻塞操作,在线程池中执行,不阻塞事件循环
return {"status": "ok"}
# 异步路由:在事件循环中执行
@router.get("/async")
async def async_endpoint():
await asyncio.sleep(1) # 异步等待,不占用线程
return {"status": "ok"}
关键规则:如果路由函数内使用了 await,必须声明为 async def;如果路由函数内都是同步操作(如 CPU 密集型计算),使用 def 即可,FastAPI 会自动在线程池中执行。
9.2 什么时候不该用 async
有一个常见的误解:"所有的 FastAPI 路由都应该声明为 async def 以获得更好的性能。"实际情况相反------声明了 async def 但没有 await 任何异步操作的路由,与 def 版本性能几乎一样,但如果函数内包含同步阻塞操作(如 time.sleep(5)),async def 版本会阻塞整个事件循环,而 def 版本会被分配到线程池执行,不会阻塞其他请求。
规则:函数的声明方式应该匹配函数的实际行为,而非"全都用 async"。
| 场景 | 声明方式 | 原因 |
|---|---|---|
| 使用 asyncpg / httpx.AsyncClient | async def |
需要 await 异步操作 |
| 使用 psycopg2 / requests | def |
同步库,在线程池执行 |
| CPU 密集型计算 | def |
在线程池执行,避免阻塞事件循环 |
| 混合(async DB + 同步文件读写) | async def + run_in_executor |
同步部分交给线程池 |
10. 后台任务
BackgroundTasks 是 FastAPI 内置的轻量后台任务机制,适用于"响应返回后再执行、但不需要独立 Worker 进程"的场景:
python
from fastapi import BackgroundTasks
def send_creation_notification(book_id: int, book_title: str):
"""发送图书创建通知(邮件/站内信/Webhook)"""
# 这个函数在后台线程中执行,不影响 HTTP 响应
email_service.send(
to="admin@example.com",
subject=f"新书上架:{book_title}",
body=f"图书 #{book_id}「{book_title}」已创建",
)
@router.post("/", status_code=201)
async def create_book(
book: BookCreate,
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_db_session),
):
new_book = await book_service.create(db, book)
background_tasks.add_task(send_creation_notification, new_book.id, new_book.title)
return new_book
BackgroundTasks 的适用边界在于:任务执行在同一个进程内,服务重启会丢失;任务不能太重(如处理 10 万条数据),否则会耗尽进程资源。当任务需要独立扩展(如批量邮件发送)、需要高可靠性(如订单支付回调)、或有复杂的重试策略时,应该使用 Celery 或 Kafka------这个话题将在后续文章中展开。
11. OpenAPI 文档定制
FastAPI 自动生成的文档可以通过参数定制为符合团队风格的 API 文档:
python
app = FastAPI(
title="图书管理 API",
description="""
图书管理系统的 REST API,提供图书的 CRUD、搜索、分类管理功能。
## 认证方式
所有需要认证的接口在请求头中携带 `Authorization: Bearer <token>`。
## 错误码规范
- `BOOK_NOT_FOUND`: 图书不存在
- `INVALID_ISBN`: ISBN 格式错误
""",
version="1.0.0",
docs_url="/docs", # Swagger UI 路径
redoc_url="/redoc", # ReDoc 路径
openapi_url="/openapi.json",
)
# 为路由添加详细描述和示例
@router.post(
"/",
status_code=201,
summary="创建新图书",
response_description="成功创建的图书信息",
)
async def create_book(book: BookCreate, ...):
...
summary 和 description 直接影响 Swagger UI 中的展示效果。Pydantic 模型中的 Field(description="...", examples=[...]) 也会被渲染到文档中,帮助调用方理解参数含义。
12. 完整实战:图书管理 REST API
将上述知识点串联成一个可运行的图书管理 API:
python
# app/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
@asynccontextmanager
async def lifespan(app: FastAPI):
await init_db()
yield
await close_db()
app = FastAPI(
title="图书管理 API",
version="1.0.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
app.middleware("http")(add_request_id_and_timing)
app.include_router(books.router)
python
# app/routes/books.py
from fastapi import APIRouter, Depends, Query, HTTPException, BackgroundTasks
from sqlalchemy.ext.asyncio import AsyncSession
router = APIRouter(prefix="/api/v1/books", tags=["books"])
@router.get("/", response_model=list[BookResponse])
async def list_books(
db: AsyncSession = Depends(get_db_session),
paging: dict = Depends(pagination),
order: dict = Depends(ordering),
search: str | None = Query(None, min_length=1),
):
books, total = await book_service.list_books(
db, search=search, offset=paging["offset"],
limit=paging["limit"], order=order,
)
return books
@router.get("/{book_id}", response_model=BookDetailResponse)
async def get_book(
book_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db_session),
):
book = await book_service.get_by_id(db, book_id)
if not book:
raise HTTPException(status_code=404, detail={"message": f"图书 #{book_id} 不存在", "code": "BOOK_NOT_FOUND"})
return book
@router.post("/", status_code=201, response_model=BookDetailResponse)
async def create_book(
book: BookCreate,
db: AsyncSession = Depends(get_db_session),
background_tasks: BackgroundTasks,
):
new_book = await book_service.create(db, book)
background_tasks.add_task(notify_new_book, new_book.id, new_book.title)
return new_book
这个 API 的结构体现了 FastAPI 的最佳实践:路由层极薄(仅参数接收和异常抛出),依赖注入管理资源生命周期,Pydantic 模型管理数据校验和序列化,后台任务处理非关键路径操作。当数据访问层需要替换(从内存字典切换到 SQLAlchemy),只需修改 book_service 的实现,路由层完全不受影响。
总结
FastAPI 的核心设计哲学是"声明式"------类型注解同时驱动校验、序列化和文档生成。理解这个哲学后,开发 REST API 的体验会从"写代码"变成"描述接口"。依赖注入系统是整个框架的骨架:它能简化资源管理、消除样板代码,但需要理解缓存机制以避免"同一请求多次创建资源"的陷阱。
文中涉及的依赖注入缓存机制和 ASGI 中间件洋葱模型,在 Python 进阶系列中关于函数闭包和异步事件循环的文章中有更深入的底层原理分析,感兴趣的读者可以参考。
如果这篇文章对构建 FastAPI 项目有帮助,欢迎点赞、收藏、关注。大家的支持是持续输出高质量技术内容的动力。