Chapter 02

路径操作与路由

掌握路径参数、查询参数、请求体的完整用法,理解三者的选择原则,并用 APIRouter 构建模块化、可维护的路由结构。

路径参数

路径参数是 URL 路径中的可变部分,用花括号 {param} 声明。FastAPI 会自动将 URL 字符串转换为函数参数声明的类型:

from fastapi import FastAPI, HTTPException
from enum import Enum

app = FastAPI()

# ── 基本路径参数(自动类型转换)─────────────────────────
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    # FastAPI 自动将 URL 中的 "123" 转换为整数 123
    # 如果传入 "abc",自动返回 422 验证错误
    if user_id <= 0:
        raise HTTPException(status_code=404, detail="用户不存在")
    return {"user_id": user_id, "name": f"用户{user_id}"}

# ── 枚举路径参数(限制合法值)────────────────────────────
class ModelName(str, Enum):
    gpt4 = "gpt4"
    claude = "claude"
    gemini = "gemini"

@app.get("/models/{model_name}")
async def get_model(model_name: ModelName):
    # 传入非枚举值时自动返回 422,无需手动验证
    descriptions = {
        ModelName.gpt4: "OpenAI GPT-4",
        ModelName.claude: "Anthropic Claude",
        ModelName.gemini: "Google Gemini",
    }
    return {"model": model_name, "description": descriptions[model_name]}

# ── 路径参数包含斜杠(文件路径)──────────────────────────
@app.get("/files/{file_path:path}")
async def read_file(file_path: str):
    # :path 修饰符允许参数包含 / 字符
    # GET /files/docs/2025/report.pdf → file_path = "docs/2025/report.pdf"
    return {"file_path": file_path}

# ── 路由顺序很重要:固定路径先于参数路径 ─────────────────
@app.get("/users/me")       # 必须在 /users/{user_id} 之前注册!
async def get_current_user():
    return {"user": "当前登录用户"}
路由注册顺序 FastAPI 按路由注册顺序匹配请求。/users/me 必须在 /users/{user_id} 之前注册,否则 "me" 会被当作 user_id 处理,导致类型验证错误("me" 无法转换为 int)。

查询参数

查询参数是 URL 中 ? 后的键值对。在函数签名中,非路径参数的简单类型参数自动识别为查询参数:

from fastapi import FastAPI, Query
from typing import Annotated

app = FastAPI()

# ── 必填与可选查询参数 ────────────────────────────────────
@app.get("/items/")
async def list_items(
    skip: int = 0,                # 可选,默认 0
    limit: int = 10,              # 可选,默认 10
    q: str | None = None,        # 可选,默认 None
    active: bool = True           # bool 参数:?active=true/false/1/0
):
    # GET /items/?skip=20&limit=5&q=python&active=false
    result = {"skip": skip, "limit": limit, "active": active}
    if q:
        result["query"] = q
    return result

# ── Query() 进阶验证 ──────────────────────────────────────
@app.get("/search/")
async def search(
    # Annotated + Query() 是 FastAPI 推荐的现代写法
    q: Annotated[str, Query(
        min_length=2,
        max_length=50,
        description="搜索关键词,2-50 个字符",
        example="FastAPI"
    )],
    page: Annotated[int, Query(ge=1, description="页码,从 1 开始")] = 1,
    size: Annotated[int, Query(ge=1, le=100, description="每页条数")] = 20,
):
    return {"query": q, "page": page, "size": size}

# ── 多值查询参数(列表)──────────────────────────────────
@app.get("/filter/")
async def filter_items(
    tags: Annotated[list[str], Query()] = []
):
    # GET /filter/?tags=python&tags=api&tags=fastapi
    # tags = ["python", "api", "fastapi"]
    return {"tags": tags}

请求体

POST、PUT、PATCH 方法通常携带 JSON 请求体。将 Pydantic 模型作为函数参数,FastAPI 自动从请求体解析并验证:

from fastapi import FastAPI, Body
from pydantic import BaseModel, Field
from typing import Annotated

app = FastAPI()

class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None

class User(BaseModel):
    username: str
    full_name: str | None = None

# ── 单个请求体 ────────────────────────────────────────────
@app.post("/items/")
async def create_item(item: Item):
    # 请求体:{"name": "Foo", "price": 9.99}
    item_dict = item.model_dump()
    if item.tax:
        item_dict["price_with_tax"] = item.price + item.tax
    return item_dict

# ── 多个请求体(自动包装为嵌套 JSON)──────────────────────
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, user: User):
    # 请求体变为:{"item": {...}, "user": {...}}
    return {"item_id": item_id, "item": item, "user": user}

# ── 混合:路径 + 查询 + 请求体 ───────────────────────────
@app.put("/items/{item_id}/update")
async def update_item_with_extra(
    item_id: int,                # 路径参数
    q: str | None = None,      # 查询参数
    item: Item | None = None, # 请求体(可选)
):
    result = {"item_id": item_id}
    if q:
        result["q"] = q
    if item:
        result["item"] = item.model_dump()
    return result

路径参数 vs 查询参数 vs 请求体

三种参数类型各有适用场景,遵循 REST 设计原则来选择:

参数选择原则 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 路径参数 /users/{user_id} → 资源的唯一标识符(ID) → 必填,是 URL 的一部分 → 例:GET /users/123、GET /orders/abc-def 查询参数 /items?page=2&sort=price → 过滤、排序、分页等修饰条件 → 通常可选,有合理默认值 → 例:GET /items?category=书籍&sort=price&page=2 请求体 {"name": "value"} → 创建或更新资源的完整数据 → 用于 POST/PUT/PATCH 方法 → 数据量大、结构复杂、包含敏感信息(密码)时使用

APIRouter:模块化路由

当应用增长时,将所有路由放在 main.py 中会变得难以维护。APIRouter 允许将路由按功能模块拆分:

# ── app/routers/users.py ──────────────────────────────────
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel

# prefix:所有路由加上 /users 前缀
# tags:Swagger UI 中的分组标签
router = APIRouter(
    prefix="/users",
    tags=["用户管理"],
    responses={404: {"description": "用户不存在"}}
)

class UserCreate(BaseModel):
    username: str
    email: str
    password: str

class UserResponse(BaseModel):
    id: int
    username: str
    email: str
    # 注意:没有 password 字段,不会泄露密码

# 路由路径 "/": 完整路径是 /users/
@router.get("/", response_model=list[UserResponse])
async def list_users():
    return [{"id": 1, "username": "alice", "email": "alice@example.com"}]

@router.post("/", response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate):
    # response_model=UserResponse 过滤掉 password
    return {"id": 1, "username": user.username, "email": user.email}

@router.get("/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
    if user_id != 1:
        raise HTTPException(status_code=404, detail="用户不存在")
    return {"id": user_id, "username": "alice", "email": "alice@example.com"}

@router.delete("/{user_id}", status_code=204)
async def delete_user(user_id: int):
    return None  # 204 No Content,无返回体
# ── app/main.py ───────────────────────────────────────────
from fastapi import FastAPI
from app.routers import users, items, auth

app = FastAPI(title="我的 API", version="1.0.0")

# 注册路由模块
app.include_router(users.router)
app.include_router(items.router)
app.include_router(auth.router, prefix="/auth", tags=["认证"])

@app.get("/health")
async def health():
    return {"status": "ok"}

响应模型与数据过滤

response_model 参数是 FastAPI 最重要的安全特性之一:它定义了 API 返回的数据格式,自动过滤掉不在模型中的字段(如密码):

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class UserInDB(BaseModel):
    """数据库中存储的完整用户数据"""
    id: int
    username: str
    email: str
    hashed_password: str  # 敏感字段
    is_admin: bool

class UserPublic(BaseModel):
    """对外公开的用户数据(不含敏感字段)"""
    id: int
    username: str
    email: str

# response_model 确保:即使函数返回 UserInDB,
# 实际响应中只包含 UserPublic 定义的字段
@app.get("/users/{user_id}", response_model=UserPublic)
async def get_user(user_id: int):
    # 从数据库获取完整数据(包含 hashed_password)
    user_from_db = UserInDB(
        id=user_id, username="alice",
        email="alice@example.com",
        hashed_password="$2b$12$xxx...",
        is_admin=False
    )
    # FastAPI 自动提取 UserPublic 需要的字段,过滤 hashed_password 和 is_admin
    return user_from_db

# response_model_exclude_unset:只返回明确设置的字段(PATCH 操作常用)
@app.patch("/users/{user_id}", response_model=UserPublic, response_model_exclude_unset=True)
async def partial_update_user(user_id: int, user: UserPublic):
    return user
本章小结 FastAPI 的三种参数类型:路径参数(资源标识)、查询参数(过滤修饰)、请求体(资源数据)。APIRouter 实现路由模块化,response_model 实现响应数据安全过滤。下一章深入 Pydantic v2,学习如何定义复杂的数据验证逻辑。