FastAPI 的路由表面看和 Flask 差不多,真正拉开差距的是两件事:参数从声明里自动解析校验,以及 Depends 依赖注入。前者省掉手动取参和校验的代码,后者让鉴权、数据库连接这类横切逻辑不用到处复制粘贴。这篇把这两块讲透。
三种参数来源
FastAPI 按参数声明的类型自动判断它从哪来,不需要你手动 request.args.get()。
路径参数
写在 URL 路径里的 {name},函数参数同名接收,类型决定转换和校验:
@app.get("/users/{user_id}") async def get_user(user_id: int): return {"user_id": user_id}
user_id: int 会做两件事:把字符串 "123" 转成整数 123;不是整数直接 422。路径里还能用枚举约束取值:
from enum import Enum class Role(str, Enum): admin = "admin" user = "user" @app.get("/roles/{role}") async def get_role(role: Role): return {"role": role.value}
访问 /roles/admin 正常,/roles/root 直接被拒——枚举值成了输入白名单。
查询参数
不在路径里、又有默认值的「简单类型」参数,会被当作查询参数(?key=value):
@app.get("/items/") async def list_items(skip: int = 0, limit: int = 10, q: str | None = None): return {"skip": skip, "limit": limit, "q": q}
必填查询参数就给类型但不给默认值:async def search(q: str)。Optional[str] = None 和 str | None = None 等价,后者要 Python 3.10+。
请求体
复杂结构用 Pydantic 模型接收,FastAPI 自动解析 JSON body、校验字段、转成对象:
from pydantic import BaseModel class Item(BaseModel): name: str price: float tags: list[str] = [] @app.post("/items/") async def create_item(item: Item): return {"name": item.name, "price": item.price}
三种来源可以同时出现,FastAPI 按规则各取所需,不会混:
@app.put("/items/{item_id}") async def update_item(item_id: int, item: Item, q: str | None = None): ...
item_id 来自路径,item 来自 body,q 来自查询字符串。
依赖注入:Depends
这是 FastAPI 最值得花时间理解的设计。语法是一个函数(或可调用对象)用 Depends 声明为参数,框架在收到请求时先调用它、把返回值注入。
基础:复用一段逻辑
from fastapi import Depends def get_query(q: str | None = None): return {"q": q} @app.get("/search/") async def search(dep: dict = Depends(get_query)): return dep
看起来多此一举?当逻辑变复杂就显价值了——比如做公共的参数解析、权限判断、数据预处理。
层级依赖
依赖还能依赖别的依赖,形成调用链:
def get_token(token: str | None = None): if not token: raise HTTPException(status_code=401, detail="missing token") return token def get_current_user(token: str = Depends(get_token)): user = lookup_user(token) # 伪代码:查库 if not user: raise HTTPException(status_code=401, detail="invalid token") return user @app.get("/me") async def me(user: dict = Depends(get_current_user)): return user
请求进来时执行顺序是 get_token → get_current_user → me。任一层抛 HTTPException,后续不执行,直接返回对应状态码。鉴权逻辑集中在一处,所有需要登录的接口只写一行 Depends(get_current_user)。
yield 依赖:资源的获取与释放
需要「用完即清理」的资源(数据库连接、锁、事务)用 yield:
from contextlib import asynccontextmanager def get_db(): db = SessionLocal() try: yield db finally: db.close() # 请求结束一定执行,不论成功或异常 @app.get("/posts/") async def posts(db: Session = Depends(get_db)): return db.query(Post).all()
yield 之前是「请求前」,之后是「请求后」。即使 handler 抛异常,finally 里的 db.close() 也会跑。这和 try/finally 语义一致,只是由框架在正确的时机驱动。
依赖缓存
同一个依赖在同一个请求里被多处引用时,默认只执行一次,结果缓存复用:
async def get_user(user: dict = Depends(get_current_user)): ... async def update_profile(user: dict = Depends(get_current_user)): ...
同一请求里多处都依赖 get_current_user(比如 handler 本体 + 它的子依赖都声明了它),默认 use_cache=True,整个请求生命周期内只执行一次、结果缓存复用——对「查库取用户」这类操作能省一次往返。只有需要每次都重新执行(如每次都重新校验 token 是否刚过期)时,才用 Depends(get_current_user, use_cache=False) 关闭缓存。
依赖和路由的边界
一个常见误区是把业务逻辑全塞进依赖里。依赖适合放横切关注点:鉴权、DB session、分页参数解析、请求日志。具体业务处理留在 handler 本体。这样每个接口一眼能看懂「它要什么、做什么」。
下一篇讲 Pydantic——上面那些 Item、get_current_user 返回的 user 对象,背后都是用 Pydantic 精确描述「数据长什么样、哪些合法」的。它会让你彻底摆脱手写 if not isinstance(x, int) 的校验代码。