写FastAPI后端两年,踩过最大的坑不是语法错误,而是那些文档里没写明白的"潜规则"。这篇分享7个真正帮我省下大量调试时间的技巧,每个都带完整代码示例。

1. 部分更新用exclude_unset,别让None覆盖真实数据

打开网易新闻 查看精彩图片

写PATCH接口时,如果只传了name字段,默认情况下Pydantic会把所有可选字段都序列化成None。这意味着email和age会被null覆盖,用户数据莫名其妙就丢了。

打开网易新闻 查看精彩图片

解法是在model_dump()里加exclude_unset=True,这样只返回客户端实际发送的字段:

update_data = body.model_dump(exclude_unset=True)

{"name": "Alice"} 而不是 {"name": "Alice", "email": None, "age": None}

2. HTTPException也能带自定义Header

需要返回401并告诉客户端token过期、多久后重试?别手动拼JSONResponse了,HTTPException直接支持headers参数:

raise HTTPException(
status_code=401,
detail="Token expired",
headers={"X-Error-Code": "TOKEN_EXPIRED", "X-Retry-After": "3600"}
)

代码更干净,错误响应格式也更统一。

3. yield做依赖注入,自动清理资源

数据库连接、文件句柄、临时资源最怕泄露。用yield把依赖拆成"初始化→使用→清理"三段:

async def get_db():
db = await connect_database()
try:
yield db # 这里返回给路由使用
finally:
await db.close() # 响应发送后执行,异常时也会触发

finally块保证即使抛出异常,清理代码也会跑。路由函数里直接db=Depends(get_db)就行,完全不用操心关闭逻辑。

4. 全局异常处理器,告别满屏try/except

每个路由都写try/except是灾难。定义一个AppError异常类,注册全局handler:

@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError):
return JSONResponse(
status_code=exc.status,
content={"error": {"code": exc.code, "message": exc.message}}
)

路由里直接raise AppError("USER_NOT_FOUND", "...", 404),整个API的错误格式自动保持一致,零样板代码。

5. 文件上传必须服务端校验类型

打开网易新闻 查看精彩图片

客户端校验不可靠。收到UploadFile后,务必检查content_type:

ALLOWED_TYPES = {"image/jpeg", "image/png"}
if file.content_type not in ALLOWED_TYPES:
raise HTTPException(400, "Invalid file type")

别依赖文件名后缀,content_type才是服务端能信任的信息。

6. BackgroundTasks处理异步操作

发送邮件、生成报表这些耗时操作别阻塞响应。FastAPI内置BackgroundTasks:

from fastapi import BackgroundTasks

async def send_email(email: str):
...

@app.post("/signup")
async def signup(user: UserCreate, background_tasks: BackgroundTasks):
background_tasks.add_task(send_email, user.email)
return {"message": "User created"}

响应立即返回,邮件在后台慢慢发。比直接上Celery轻量多了,适合中小项目。

7. 用Depends复用通用校验逻辑

分页参数、权限检查、API版本这些横切关注点,抽成依赖函数:

async def pagination_params(page: int = 1, size: int = 20):
return {"skip": (page-1)*size, "limit": size}

@app.get("/items")
async def list_items(pagination: dict = Depends(pagination_params)):
...

路由代码专注于业务逻辑,通用机制一处定义到处复用。测试时也方便mock掉。

这7个技巧都不是什么高深技术,但文档不会告诉你什么时候该用、坑在哪。踩过才知道,提前知道就能少熬很多夜。