分页、检索与错误结构
分页、检索与错误结构 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
这一课把「返回一堆数据」和「报一堆错」变成有设计的行为:稳定分页、可控规模、统一错误结构。目标完成后,前端可以只写一套列表渲染和一套错误处理就吃下你所有接口。
问题场景
课程库涨到 180 门,GET /courses 一次性全量返回,前端渲染开始卡;更糟的是某个调用方传了 page=0、limit=99999,本机服务直接被拖死。而各接口报错五花八门:有的返回 {"msg": ...},有的抛 500——前端没法统一提示。这课用一套约定同时解决两件事。
核心解释
分页的稳定三件套:page(从 1 起)+ limit(带上限)+ total/items 外壳。
from fastapi import FastAPI, Query, Request
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError):
return JSONResponse(
status_code=exc.status,
content={"error": {"category": exc.category, "message": exc.message}},
)
class AppError(Exception):
def __init__(self, status: int, category: str, message: str):
self.status, self.category, self.message = status, category, message
@app.get("/courses")
def list_courses(
page: int = Query(default=1, ge=1), # 页码从 1 起
limit: int = Query(default=20, ge=1, le=100), # le=100 是规模上限
keyword: str | None = Query(default=None, max_length=50),
):
rows = COURSES # 实际来自数据库
if keyword:
rows = [c for c in rows if keyword.lower() in c["title"].lower()]
start = (page - 1) * limit
items = rows[start:start + limit]
return {"total": len(rows), "page": page, "limit": limit, "items": items}
设计决策逐条钉住:
le=100限制规模。没有上限的分页参数等于邀请任何人全量拉库;le让超限请求直接 422,本站/api/run对输出截断、P06 对脚本限时,是同一个「限制请求规模」的思想。total + items外壳全站一致。P07 埋的这个伏笔在此定型:前端分页组件拿到total就能算总页数,翻页、筛选、搜索共用一个外壳。- 错误结构对齐本站契约。
{error: {category, message}},category 取稳定枚举(invalid_param/not_found/conflict/internal),message 给人读。前端拿到 category 能分支处理,拿到 message 能直接展示。Pydantic 校验出的 422 会在exception_handler里被转换成同一结构。 - 越界分页不报错。
page=99超出范围返回空items加真实total,而不是 404——「查无此页」是正常业务状态,不是资源不存在。
示例拆解
四种请求覆盖行为矩阵:
curl "http://127.0.0.1:8100/courses?page=1&limit=2"
# {"total":180,"page":1,"limit":2,"items":[...2 条...]} 外壳完整
curl "http://127.0.0.1:8100/courses?page=1&limit=500"
# 422,limit ≤ 100 被拦
curl "http://127.0.0.1:8100/courses?page=1&limit=2&keyword=fastapi"
# total 变小,items 只含命中标题
curl "http://127.0.0.1:8100/courses?page=999"
# {"total":180,"page":999,"items":[]} 空页不是错误
预期:外壳字段在每种情况下都齐全,前端逻辑不需要为任何一种情况写特例。搜索命中数变化只体现在 total 和 items 里,翻页状态由 page/limit 原样回显——这三个字段凑成自描述的分页协议。
动手任务
- 给
/courses加track筛选参数(限定枚举值),验证非法值返回 422 且错误结构统一。 - 写一个
AppError子类NotFound,在/courses/{code}查无课程时抛出,确认响应体是{error:{category:"not_found",...}}而不是 FastAPI 默认格式。 - 用本站前端思路写一个翻页请求:读
total算总页数,验证最后一页items长度不足 limit 时渲染正常。
常见错误
- page 从 0 起:前端按习惯传 0,
ge=1直接 422;翻页起点两边必须白纸黑字一致。 - limit 无上限:
le漏写,一次请求拉全库,本机服务内存和带宽一起遭殃。 - 错误格式随接口漂移:有的
{error:...}有的{message:...},前端写两套 if;全走 exception_handler 统一出口。 - 把空结果当错误:筛选无命中返回 404,前端只能报「出错了」;空列表是 200 加空 items。
检查与复盘
自查清单:
- 分页参数有默认值、下限和上限,且行为在 /docs 里可验证
- 所有可预期错误都走统一结构,category 是有限枚举
- 空页、无命中、超限三种边界行为说得清并演示过
参考资料
- FastAPI 官方教程:Query Parameters and String Validations —— Query 约束全集
- FastAPI 官方教程:Handling Errors —— 自定义异常处理器
读完这一课了?
标记完成后,工作台会安排接下来的学习。