FastAPI Patterns
Async Traps
- - Mixing sync database drivers (psycopg2, PyMySQL) in async endpoints blocks the event loop — use async drivers (asyncpg, aiomysql) or run sync code in INLINECODE0
- INLINECODE1 in async endpoints blocks everything — use
await asyncio.sleep() instead - CPU-bound work in async endpoints starves other requests — offload to
ProcessPoolExecutor or background workers - Async endpoints calling sync functions that do I/O still block — the entire call chain must be async
Pydantic Validation
- - Default values in models become shared mutable state:
items: list = [] shares the same list across requests — use INLINECODE5 - INLINECODE6 doesn't make a field optional in the request — add
= None or use INLINECODE8 - Pydantic v2 uses
model_validate() not parse_obj(), and model_dump() not .dict() — v1 methods are deprecated - Use
Annotated[str, Field(min_length=1)] for reusable validated types instead of repeating constraints
Dependency Injection
- - Dependencies run on every request by default — use
lru_cache on expensive dependencies or cache in app.state for singletons - INLINECODE15 without an argument reuses the type hint as the dependency — clean but can confuse readers
- Nested dependencies form a DAG — if A depends on B and C, and both B and C depend on D, D runs once (cached per-request)
- INLINECODE16 dependencies for cleanup (DB sessions, file handles) — code after yield runs even if the endpoint raises
Lifespan and Startup
- -
@app.on_event("startup") is deprecated — use lifespan async context manager - Store shared resources (DB pool, HTTP client) in
app.state during lifespan, not as global variables - Lifespan runs once per worker process — with 4 Uvicorn workers you get 4 DB pools
CODEBLOCK0
Request/Response
- - Return
dict from endpoints, not Pydantic models directly — FastAPI handles serialization and it's faster - Use
status_code=201 on POST endpoints returning created resources — 200 is the default but semantically wrong - INLINECODE22 with
media_type="text/plain" for non-JSON responses — returning a string still gets JSON-encoded otherwise - Set
response_model_exclude_unset=True to omit None fields from response — cleaner API output
Error Handling
- -
raise HTTPException(status_code=404) — don't return Response objects for errors, it bypasses middleware - Custom exception handlers with
@app.exception_handler(CustomError) — but remember they don't catch HTTPException - Use
detail= for user-facing messages, log the actual error separately — don't leak stack traces
Background Tasks
- -
BackgroundTasks runs after the response is sent but still in the same process — not suitable for long-running jobs - Tasks execute sequentially in order added — don't assume parallelism
- If a background task fails, the client never knows — add your own error handling and alerting
Security
- -
OAuth2PasswordBearer is for documentation only — it doesn't validate tokens, you must implement that in the dependency - CORS middleware must come after exception handlers in middleware order — or errors won't have CORS headers
- INLINECODE30 in path operation, not in router — dependencies on routers affect all routes including health checks
Testing
- -
TestClient runs sync even for async endpoints — use httpx.AsyncClient with ASGITransport for true async testing - Override dependencies with
app.dependency_overrides[get_db] = mock_db — cleaner than monkeypatching - INLINECODE35 context manager ensures lifespan runs — without
with TestClient(app) as client: startup/shutdown hooks don't fire
FastAPI 模式
异步陷阱
- - 在异步端点中混用同步数据库驱动(psycopg2、PyMySQL)会阻塞事件循环——应使用异步驱动(asyncpg、aiomysql)或通过 runinexecutor 运行同步代码
- 异步端点中的 time.sleep() 会阻塞所有操作——应改用 await asyncio.sleep()
- 异步端点中的 CPU 密集型任务会饿死其他请求——应卸载到 ProcessPoolExecutor 或后台工作进程
- 调用执行 I/O 操作的同步函数的异步端点仍然会阻塞——整个调用链必须保持异步
Pydantic 验证
- - 模型中的默认值会成为共享的可变状态:items: list = [] 会在请求间共享同一个列表——应使用 Field(defaultfactory=list)
- Optional[str] 并不会使字段在请求中变为可选——需添加 = None 或使用 Field(default=None)
- Pydantic v2 使用 modelvalidate() 而非 parseobj(),使用 modeldump() 而非 .dict()——v1 方法已弃用
- 使用 Annotated[str, Field(min_length=1)] 创建可复用的验证类型,而非重复编写约束条件
依赖注入
- - 默认情况下,依赖项在每个请求上都会运行——对昂贵的依赖项使用 lru_cache 或在 app.state 中缓存单例
- 不带参数的 Depends() 会复用类型提示作为依赖项——简洁但可能让读者困惑
- 嵌套依赖项形成有向无环图——如果 A 依赖 B 和 C,而 B 和 C 都依赖 D,则 D 只运行一次(按请求缓存)
- 使用 yield 依赖项进行清理(数据库会话、文件句柄)——即使端点抛出异常,yield 后的代码也会执行
生命周期与启动
- - @app.on_event(startup) 已弃用——应使用 lifespan 异步上下文管理器
- 在生命周期期间将共享资源(数据库连接池、HTTP 客户端)存储在 app.state 中,而非使用全局变量
- 生命周期在每个工作进程中运行一次——使用 4 个 Uvicorn 工作进程会得到 4 个数据库连接池
python
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app):
app.state.db = await create_pool()
yield
await app.state.db.close()
app = FastAPI(lifespan=lifespan)
请求/响应
- - 从端点返回 dict,而非直接返回 Pydantic 模型——FastAPI 会处理序列化,且速度更快
- 在返回已创建资源的 POST 端点上使用 statuscode=201——默认是 200,但语义上不正确
- 对非 JSON 响应使用带 mediatype=text/plain 的 Response——否则返回字符串仍会被 JSON 编码
- 设置 responsemodelexclude_unset=True 以省略响应中的 None 字段——使 API 输出更简洁
错误处理
- - 使用 raise HTTPException(statuscode=404)——不要为错误返回 Response 对象,这会绕过中间件
- 使用 @app.exceptionhandler(CustomError) 自定义异常处理器——但注意它们不会捕获 HTTPException
- 对用户可见的消息使用 detail=,单独记录实际错误——不要泄露堆栈跟踪信息
后台任务
- - BackgroundTasks 在响应发送后执行,但仍处于同一进程中——不适合长时间运行的任务
- 任务按添加顺序依次执行——不要假设并行性
- 如果后台任务失败,客户端永远不会知道——需自行添加错误处理和告警
安全
- - OAuth2PasswordBearer 仅用于文档——它不验证令牌,你必须在依赖项中实现验证
- CORS 中间件在中间件顺序中必须位于异常处理器之后——否则错误将没有 CORS 头
- 在路径操作中使用 Depends(getcurrentuser),而非在路由器中——路由器上的依赖项会影响所有路由,包括健康检查
测试
- - TestClient 即使对异步端点也以同步方式运行——使用带 ASGITransport 的 httpx.AsyncClient 进行真正的异步测试
- 使用 app.dependencyoverrides[getdb] = mock_db 覆盖依赖项——比猴子补丁更简洁
- TestClient 上下文管理器确保生命周期运行——没有 with TestClient(app) as client: 则启动/关闭钩子不会触发