FastAPI 系列
路径、查询和请求体为什么要分开声明(当前篇)
一个 HTTP 请求里至少有三类输入:路径上的资源标识、问号后面的过滤条件、以及 JSON 体。FastAPI 把它们映射到不同的函数参数。混在一起写,OpenAPI 会乱,校验也会在错误的位置失败。

路径参数:资源是谁
路径参数写在 URL 模板里,通常表示「哪一条资源」:
from typing import Annotated
from fastapi import FastAPI, Path
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(
item_id: Annotated[int, Path(ge=1, description="商品 ID")],
):
return {"item_id": item_id}
/items/3 会得到整数 3;/items/foo 返回 422。路径参数默认必填——它本来就是地址的一部分。
查询参数:过滤和开关
查询参数跟在 ? 后面,适合分页、搜索、可选开关:
from typing import Annotated
from fastapi import Query
@app.get("/items/")
async def list_items(
q: Annotated[str | None, Query(max_length=50)] = None,
skip: Annotated[int, Query(ge=0)] = 0,
limit: Annotated[int, Query(ge=1, le=100)] = 20,
):
return {"q": q, "skip": skip, "limit": limit}
有默认值就是可选。需要必填查询参数时,去掉默认值即可。列表查询可以用 list[str],对应重复的 ?tag=a&tag=b。
请求体:要写入的数据
POST / PUT / PATCH 的 JSON 体用 Pydantic 模型,不要把整段 JSON 收成 dict:
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
tags: list[str] = []
@app.post("/items/")
async def create_item(item: Item):
return item
FastAPI 看到参数是 Pydantic 模型,就按 request body 解析。GET 通常没有 body,不要把模型塞进 GET。
混写时 FastAPI 怎么判断
拿不准就显式标注 Path()、Query()、Body()。现代写法是 Annotated[int, Path(ge=1)],比默认参数里塞 Path() 更清晰。
Header 与 Cookie
它们也是输入,但不是路径或查询:
from fastapi import Header, Cookie
@app.get("/context")
async def read_context(
user_agent: Annotated[str | None, Header()] = None,
session_id: Annotated[str | None, Cookie()] = None,
):
return {"user_agent": user_agent, "session_id": session_id}
HTTP 头名字里的连字符会转成下划线:User-Agent → user_agent。鉴权头后面会交给 OAuth2PasswordBearer,不要自己手拆 Authorization 除非有特殊协议。
常见误区
把过滤条件写进路径
/items/search/red/cheap 很难演进。搜索用查询参数:/items?color=red&max_price=20。
GET 带 JSON 体
很多客户端、缓存和代理不按规范处理 GET body。过滤条件放 Query。
所有输入都收成一个 dict
校验、文档和编辑器补全都没了。下一篇会把 body 再拆成入参模型和出参模型。
小结
路径回答「哪一个」,查询回答「怎么筛」,请求体回答「要写入什么」。分开声明后,422 错误会告诉你是哪一类字段错了。下一篇用 Pydantic 把契约写完整。
参考资料
路径、查询和请求体为什么要分开声明
https://lautung.com/archives/fastapi-03-params
评论