3. FastAPI路由与请求处理机制
FastAPI 的核心工作方式可以概括为一句话:用装饰器把 Python 函数注册成某个 URL 路径上的 HTTP 方法处理器,框架负责解析请求、校验参数、序列化响应。理解这条链路,才能真正掌控接口行为,而不是停留在“能跑就行”。本节围绕路径操作、HTTP 方法、请求参数解析、响应模型与状态码控制五个环节展开。
路径操作与 HTTP 方法
路径操作装饰器由“HTTP 方法 + 路径”组成,例如 @app.get("/items/{item_id}")。同一个路径可以绑定多个方法,FastAPI 会按方法分派到不同函数。路径中的 {item_id} 是路径参数占位符,函数形参名必须与占位符一致,否则框架无法注入。
from fastapi import FastAPI, Path, Query, status
from pydantic import BaseModel, Field
app = FastAPI(title="Demo API")
class ItemIn(BaseModel):
name: str = Field(min_length=1, max_length=50)
price: float = Field(gt=0)
tags: list[str] = []
class ItemOut(BaseModel):
id: int
name: str
price: float
@app.get("/items/{item_id}", response_model=ItemOut)
def get_item(
item_id: int = Path(ge=1, description="商品ID,从1开始"),
q: str | None = Query(default=None, max_length=20),
):
return {"id": item_id, "name": f"item-{item_id}", "price": 9.9, "q": q}
这里 Path(ge=1) 表示路径参数必须大于等于 1,Query(max_length=20) 限制查询串长度。若请求 /items/0,FastAPI 会在进入函数前返回 422,并给出字段级错误定位,函数体根本不会执行——这是“声明式校验”的关键价值。
请求参数解析的优先级与来源
FastAPI 依据参数类型和声明自动判断参数来源,规则如下:路径占位符对应路径参数;简单类型(int、str、bool 等)默认视为查询参数;Pydantic 模型默认视为请求体;Header、Cookie、Form、File 需显式声明。多个来源可以同时出现在一个函数签名中。
上图展示了完整链路:请求先经路由匹配提取路径参数,再进入参数解析与校验;校验失败直接返回 422,函数不执行;校验通过后执行业务逻辑,返回值经响应模型过滤,最终按状态码规则输出。理解这条链路,排障时就能快速定位问题出在哪一环。
响应模型:字段裁剪与数据契约
response_model 不只是类型标注,它会在返回前对数据进行过滤和序列化。上面的 get_item 返回了 q 字段,但 ItemOut 未声明该字段,最终响应中不会出现 q。这一机制能有效防止内部字段(如密码哈希、内部状态)意外泄露。
若希望保留额外字段,可用 response_model_exclude_unset=True 只输出显式赋值的字段,或用 response_model_include / response_model_exclude 精细控制。对于列表接口,直接写 response_model=list[ItemOut] 即可。
状态码控制
默认情况下,GET 返回 200,POST 返回 200。创建资源时更规范的做法是返回 201:
@app.post("/items", response_model=ItemOut, status_code=status.HTTP_201_CREATED)
def create_item(item: ItemIn):
return {"id": 1, "name": item.name, "price": item.price}
@app.get("/items/{item_id}")
def read_item(item_id: int):
if item_id > 100:
raise HTTPException(status_code=404, detail="Item not found")
return {"id": item_id}
当业务条件不满足时,用 HTTPException 抛出异常,FastAPI 会将其转换为对应状态码和 {"detail": ...} 响应体。注意:HTTPException 应在函数内部抛出,而不是返回错误字典——后者会得到 200 状态码,语义错误。
参数校验的边界与常见陷阱
路径参数和查询参数都支持 ge、le、gt、lt、min_length、max_length、pattern 等约束。请求体则通过 Pydantic 的 Field 声明。一个常见陷阱是:路径参数声明为 int,但客户端传入非数字字符串时,FastAPI 返回 422 而非 404。若希望“格式错误”和“资源不存在”都返回 404,需要在路由层做额外处理或自定义异常处理器。
另一个陷阱是查询参数的默认值。若声明 q: str | None = None,则 q 可选;若声明 q: str,则 q 必填,缺失时返回 422。布尔类型查询参数会接受 true、1、yes 等多种写法,但建议客户端统一使用 true/false 以避免歧义。
实操:一个可运行的完整示例
下面给出一个包含创建、查询、删除的迷你接口集,可直接复制运行并观察状态码与响应体变化:
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
app = FastAPI()
_db: dict[int, dict] = {}
_next_id = 1
class ItemIn(BaseModel):
name: str = Field(min_length=1, max_length=50)
price: float = Field(gt=0)
class ItemOut(BaseModel):
id: int
name: str
price: float
@app.post("/items", response_model=ItemOut, status_code=status.HTTP_201_CREATED)
def create_item(item: ItemIn):
global _next_id
new_id = _next_id
_next_id += 1
_db[new_id] = {"id": new_id, "name": item.name, "price": item.price}
return _db[new_id]
@app.get("/items/{item_id}", response_model=ItemOut)
def get_item(item_id: int):
if item_id not in _db:
raise HTTPException(status_code=404, detail="Item not found")
return _db[item_id]
@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_item(item_id: int):
if item_id not in _db:
raise HTTPException(status_code=404, detail="Item not found")
del _db[item_id]
启动后可用以下命令验证:curl -X POST http://127.0.0.1:8000/items -H "Content-Type: application/json" -d '{"name":"book","price":29.9}' 应返回 201 和完整对象;curl http://127.0.0.1:8000/items/999 应返回 404 和 {"detail":"Item not found"};curl -X DELETE http://127.0.0.1:8000/items/1 应返回 204 且无响应体。通过观察状态码和响应体,可以确认路由、参数解析、响应模型和状态码控制是否按预期协同工作。
status_code=204 时不应返回任何响应体,否则部分客户端会报错。FastAPI 对 204 响应会自动忽略返回值,但函数仍应保持无返回或返回 None。掌握以上机制后,接口的输入边界、输出契约和错误语义都由声明式代码统一约束,后续接入依赖注入、中间件和异步处理时,这条链路仍然是理解一切行为的基础。