1. 本课定位:是什么、为何重要
上一课服务已经能跑:
/health会回 ok,/docs也能点。但真实 API 不会只有探活——书签业务要按资源区分列表、详情、删除,还要支持搜索和分页雏形。如果所有逻辑都堆在一个main.py里用硬编码路径,文件很快膨胀到难维护。本课把「服务端如何定义 URL 形状」系统化:路径参数标识资源,查询参数负责过滤与限制,状态码表达结果语义,并用
APIRouter按资源拆分。数据仍用内存字典(进程重启即消失),为第 42 课 Body、第 44 课落库留接口形状。学完你应能实现书签的读列表、读详情、删除,并分清 404 与 422。
| 概念 | 一句话 |
|---|---|
| 路径参数 | 嵌在 URL 路径里的变量,如/bookmarks/3的3 |
| 查询参数 | ?后面的键值,如?q=python&limit=10 |
| APIRouter | 把一组路由拆到子模块,再挂到主app |
| HTTPException | 主动抛出业务错误(如 404)并返回 JSON detail |
为何重要:参数放错位置、状态码乱用,前端与联调会极度痛苦。
对比已学:
| 已学 | 本课 |
|---|---|
| 客户端「拼 URL、读状态码」 | 服务端「定义 URL、返回状态码」 |
| 函数参数来自调用方 | 参数可来自 Path / Query |
单文件main.py | 多文件APIRouter |
只有/health | 资源型/bookmarks |
2. 本质:从 HTTP 请求里取值
很多人以为「路由就是字符串匹配」。FastAPI 更进一步:根据装饰器上的路径模板 + 函数参数注解,决定如何从请求里取值;类型转换失败时自动 422,不必手写一堆 if。
上一课你返回固定 JSON;这一课 JSON 开始依赖「谁在访问、带了什么参数」。先建立「入参位置」地图:Path、Query、Body(Body 下节),再写代码才不会把过滤条件塞进路径里。
本质:路径模板 + 注解 → 自动解析与校验;业务「找不到」要自己 404。
| 入参位置 | 例子 | 适合 |
|---|---|---|
| Path | /bookmarks/{id} | 资源标识,几乎总是必填 |
| Query | ?q=&limit= | 过滤、排序、分页,常可选 |
| Body | JSON(第 42 课) | 创建/更新的结构化数据 |
GET /bookmarks/3?q=py | | 路径参数 查询参数 bookmark_id=3 q="py"3. 约束与常见坑
自动校验很爽,但也带来新坑:
"abc"变不成int时是 422,不是你业务里的 404。若一律返回 200 再在 body 里写"error",前端分支会写崩。这一节把约束和坑表列全:命名一致、REST 名词化路径、204 删除、Router 拆分。红线清楚后,综合实践里的 curl 预期才读得懂。
约束:
- 路径参数名与函数参数名一致。
- 注解成
int时,"abc"→422(校验失败),不是业务 404。 - 404表示「类型对了,但资源不存在」——要自己
HTTPException。 - REST 习惯:路径用名词资源,动作用HTTP 方法。
- 列表过滤优先 Query,保持资源标识路径稳定。
常见坑:
| 坑 | 现象 | 正确直觉 |
|---|---|---|
| 用 200 表示「没找到」 | 前端难写分支 | 应用 404 |
| 把过滤条件塞进路径 | /bookmarks/search/python难扩展 | 过滤优先 Query |
| 单文件上百路由 | 难找、易冲突 | APIRouter+prefix/tags |
| 删除仍返回大 JSON | 多余 | 可用204无正文 |
| 路径参数名与函数名不一致 | 取值错乱/校验怪 | 名字对齐 |
| 422 当 404 处理 | 联调互相甩锅 | 先看是类型错还是真没有 |
4. 路径参数 vs 查询参数(对照表)
两种参数都是「给服务端传值」,但语义不同:路径说「是哪个资源」,查询说「怎么筛选/限制这次查看」。混用会让 URL 地图混乱。
这一节用总表 + 最小代码钉牢,并介绍
Query的ge/le/description。这些约束会进 OpenAPI,文档页上的限制不是摆设。
| 路径参数 | 查询参数 | |
|---|---|---|
| 形态 | /items/5 | /items?limit=5 |
| 必填感 | 强(标识资源) | 常可选 |
| 类型转换 | 注解驱动 | 注解 +Query约束 |
| 失败 | 转类型失败 422 | 约束失败 422 |
| 书签例子 | /bookmarks/1 | ?q=python&limit=10 |
fromfastapiimportQuery@router.get("/bookmarks/{bookmark_id}")defget_one(bookmark_id:int):...@router.get("/bookmarks")deflist_all(q:str|None=None,limit:int=Query(10,ge=1,le=100),):...Query约束 | 含义 |
|---|---|
ge/le | 大于等于 / 小于等于 |
default | 不传时的默认值 |
description | 写入 OpenAPI 文档 |
小步预期:limit=0或limit=9999应 422(若设置了 ge/le)。
5. 状态码按用途归组
客户端阶段你「读」状态码;服务端阶段你「写」状态码。写错语义比写错字段更难查,因为很多客户端按码分支而不是读 detail 字符串。
本课先掌握 200/204/404/422;201 创建成功会在第 42 课 POST 时高频出现。
HTTPException是主动表达业务错误的标准方式。
| 组 | 码 | 典型场景 |
|---|---|---|
| 成功有体 | 200 | 查询详情/列表 |
| 创建成功 | 201 | POST 新建(第 42 课常用) |
| 成功无体 | 204 | DELETE 成功 |
| 客户端错 | 404 | 资源不存在 |
| 校验错 | 422 | 参数类型/范围不合法 |
fromfastapiimportHTTPExceptionraiseHTTPException(status_code=404,detail="bookmark not found")| 修改前 | 修改后 |
|---|---|
return {"error": "no"}且 200 | raise HTTPException(404, detail=...) |
| 前端只能猜 body | 前端可先看 status 再分支 |
6. 路由组织:APIRouter
单文件 demo 能跑,但书签、用户、上传一多就会「翻文件翻到哭」。
APIRouter让你按资源拆模块:统一前缀、文档 tags、主 app 只负责挂载。这一节建立目录直觉即可:
main.py创建 app 并include_router;routers/bookmarks.py写资源路由。后续课目录会再长,但「按资源拆 router」的习惯从本课开始。
main.py # 创建 app、include_router routers/bookmarks.py # prefix=/bookmarks, tags=["bookmarks"] routers/__init__.py # 使 routers 成为包| API | 作用 |
|---|---|
APIRouter(prefix=..., tags=...) | 统一前缀与文档分组 |
app.include_router(router) | 挂载到应用 |
@router.get("") | 挂在 prefix 根,如/bookmarks |
@router.get("/{id}") | 详情路径 |
注意:@router.get("")与@router.get("/")在不同版本/配置下对尾斜杠敏感;本课列表用""配合 prefix,curl 时用/bookmarks无尾斜杠即可。
7. REST 直觉(书签资源)
REST 不是教条考试,而是「调用方好猜」的地图:名词做路径,动词做方法。
/getBookmarkById这种动词路径能跑,但难扩展、难缓存、难和前端约定。用书签资源把本课三个接口钉在地图上。写操作 POST/PATCH 留给模型课;本课聚焦读与删,先把 Path/Query/状态码练熟。
| 方法 | 路径 | 含义 |
|---|---|---|
| GET | /bookmarks | 列表(可带 q、limit) |
| GET | /bookmarks/{id} | 详情 |
| DELETE | /bookmarks/{id} | 删除 |
| POST | /bookmarks | 创建(第 42 课) |
| PATCH | /bookmarks/{id} | 部分更新(第 42 课) |
| 坏路径习惯 | 更好习惯 |
|---|---|
/getBookmarks | GET /bookmarks |
/bookmarks/delete/1 | DELETE /bookmarks/1 |
/bookmarks/search/python | GET /bookmarks?q=python |
8. 落地场景:内存书签列表
数据放内存字典:实现快、零依赖,但进程重启即消失——这是刻意选择,不是缺陷。第 44 课再换 SQLite,接口形状尽量保持稳定。
场景表帮助你对照「接口行为」写代码,而不是先纠结数据库选型。
| 接口 | 行为 |
|---|---|
GET /bookmarks | 列表;q模糊标题;limit截断 |
GET /bookmarks/{id} | 详情或 404 |
DELETE /bookmarks/{id} | 删除或 404;成功 204 |
GET /health | 探活(主 app) |
内存存储直觉:
_DB = { 1: {"id": 1, "title": "...", "url": "..."}, ... }9. 小步示例:404 与 422 对照
这是本课最重要的体感实验之一。同一条「看起来像详情」的 URL,失败原因不同,状态码不同。分不清就会在联调时浪费整天。
先看表,综合实践里用 curl 亲自打一遍
99与abc。
| 请求 | 更可能状态码 | 原因 |
|---|---|---|
GET /bookmarks/1(存在) | 200 | 正常 |
GET /bookmarks/99(不存在) | 404 | 业务找不到 |
GET /bookmarks/abc(id 注解 int) | 422 | 类型校验失败 |
GET /bookmarks?limit=0(ge=1) | 422 | 范围校验失败 |
DELETE /bookmarks/1成功 | 204 | 成功无正文 |
10. 环境准备
延续 day40 虚拟环境即可;本课多一个
routers包。Windows 推荐 Cygwin/WSL 执行 heredoc。
| 项目 | 要求 |
|---|---|
| Python | 3.10+ |
| 包 | fastapi、uvicorn[standard] |
| 目录 | day41/main.py、day41/routers/bookmarks.py |
mkdir-p~/python-lab/src/day41/routerscd~/python-lab/src/day41# 激活 venv 后pipinstall'fastapi>=0.110''uvicorn[standard]>=0.27'11. 综合实践:完整可运行脚本
一次写入 router、空
__init__、main,并启动验证。请完整跑通四类 curl:过滤列表、详情、不存在、删除。422 实验单独再打一次abc。Windows 请用 Cygwin/WSL 执行;PowerShell 可手建同名文件。Uvicorn 占前台时另开终端验证。
mkdir-p~/python-lab/src/day41/routerscd~/python-lab/src/day41cat>routers/bookmarks.py<<'EOF' from fastapi import APIRouter, HTTPException, Query router = APIRouter(prefix="/bookmarks", tags=["bookmarks"]) _DB = { 1: {"id": 1, "title": "FastAPI 文档", "url": "https://fastapi.tiangolo.com"}, 2: {"id": 2, "title": "Python 官网", "url": "https://www.python.org"}, 3: {"id": 3, "title": "Real Python", "url": "https://realpython.com"}, } @router.get("") def list_bookmarks( q: str | None = None, limit: int = Query(default=10, ge=1, le=100), ): items = list(_DB.values()) if q: ql = q.lower() items = [x for x in items if ql in x["title"].lower()] return {"items": items[:limit], "total": len(items)} @router.get("/{bookmark_id}") def get_bookmark(bookmark_id: int): item = _DB.get(bookmark_id) if not item: raise HTTPException(status_code=404, detail="bookmark not found") return item @router.delete("/{bookmark_id}", status_code=204) def delete_bookmark(bookmark_id: int): if bookmark_id not in _DB: raise HTTPException(status_code=404, detail="bookmark not found") del _DB[bookmark_id] return None EOFcat>routers/__init__.py<<'EOF' EOF cat > main.py << 'EOF' from fastapi import FastAPI from routers import bookmarks app = FastAPI(title="Day41 Bookmark API", version="0.1.0") app.include_router(bookmarks.router) @app.get("/health") def health(): return {"status": "ok"} EOFuvicorn main:app--reload--host127.0.0.1--port8000验证命令(另开终端):
curl-s"http://127.0.0.1:8000/bookmarks?q=python"curl-s"http://127.0.0.1:8000/bookmarks/1"curl-s"http://127.0.0.1:8000/bookmarks/99"curl-s-o/dev/null-w"%{http_code}\n"-XDELETE"http://127.0.0.1:8000/bookmarks/2"curl-s"http://127.0.0.1:8000/bookmarks/abc"curl-s"http://127.0.0.1:8000/bookmarks?limit=0"预期要点:
| 请求 | 预期 |
|---|---|
q=python | 标题含 python 的项(大小写不敏感) |
id=1 | 200 + 对象 |
id=99 | 404 +{"detail":"..."} |
| DELETE 2 成功 | 204 |
id=abc | 422 |
limit=0 | 422 |
修改前:单文件只有/health。
修改后:资源路由拆分,列表可过滤,详情/删除语义完整。
12. 路由注册顺序直觉(了解)
固定路径与动态路径混用时,声明顺序偶尔影响匹配(例如
/bookmarks/special与/bookmarks/{id})。本课数据简单,但要知道:更具体的路径通常应先于宽泛的{id}。入门不强制踩坑演示;项目变大时若「明明写了路由却 422/404」,回来查顺序与前缀。
| 建议 | 原因 |
|---|---|
| 先写静态子路径 | 避免被{id}吃掉 |
| prefix 统一在 Router | 减少手写重复/bookmarks |
| tags 按资源 | /docs分组清晰 |
13. 常见问答
集中处理:total 是过滤前还是过滤后、204 有没有 body、内存删除刷新后为何又回来、Query 默认值会不会进 URL。
Q:total应该是过滤后长度还是全库长度?
A:本课示例用过滤后的len(items);产品里要文档写清,前后端对齐即可。
Q:204 响应可以带 JSON 吗?
A:语义上成功无正文;客户端不要依赖 204 的 body。
Q:删除后重启服务数据又在?
A:内存初始_DB写在代码里,重启会重建;第 44 课落库后才持久。
Q:不传limit会怎样?
A:使用默认 10(本课 Query default)。
14. 自我检查清单
勾完再进第 42 课。Path/Query/404/422/Router 五项是本课硬指标。
- 能解释路径参数与查询参数分工
- 会写
Query(..., ge=, le=) - 会
HTTPException(404) - 知道
abc→422、不存在 id→404 - 会
APIRouter+include_router - DELETE 成功返回 204
- 跑通过滤列表 curl
15. 与前后课衔接
| 课 | 关系 |
|---|---|
| 40 | 会起服务 → 本课定义资源 URL |
| 42 | 本课读删 → 下课 POST/PATCH Body |
| 44 | 内存_DB→ SQLite 表 |
总结
收束几句带走:路径标识资源,查询负责过滤分页;404 与 422 分工明确;Router 按资源拆分;内存库只为练接口形状。后面加 Body 和数据库时,尽量少改 URL 地图。
- 路径参数标识资源;查询参数做过滤/分页。
- 类型/范围失败 → 422;资源不存在 → 404。
- DELETE 成功常用 204;列表过滤优先 Query。
APIRouter(prefix, tags)+include_router拆分维护。- 内存字典重启即失,接口形状为后续落库铺路。
- REST:名词路径 + HTTP 方法,少用动词路径。
小练笔
先做再看答案。可选实践建议真的 curl 一遍 422,把状态码记在本子上。
题 1
/bookmarks/foo且bookmark_id: int,更常见?
A. 200 B. 404 C. 422
题 2
Query(10, ge=1, le=100)中ge含义?
题 3
列表过滤用路径还是查询参数更合适?为什么?
题 4
DELETE 成功为何常用 204?
题 5
写出「获取 id=3 的书签」的方法 + 路径示例。
题 6
判断:业务上找不到 id=99,应返回 422。
题 7
APIRouter(prefix="/bookmarks")后,列表函数装饰@router.get(""),完整路径是?
题 8
limit=1000在le=100时更可能?
A. 截断到 100 B. 422 C. 500
题 9(可选实践)
把默认limit改成 2,不传 q 时列表最多几条?动手验证。
题 10
为什么不建议/doDeleteBookmark?id=1这种路径?
小练笔参考答案
先自测再对照。意思对即可。
题 1
C
题 2
greater or equal,最小值 1。
题 3
查询参数;过滤可选、可组合,路径应保持资源标识稳定。
题 4
成功且无需返回正文时,204 语义更贴切。
题 5
GET /bookmarks/3(主机端口按本地环境)。
题 6
错(应 404)
题 7
/bookmarks(或等价无尾斜杠形式,按你框架配置)
题 8
B
题 9
最多 2 条(以你修改后运行为准)。
题 10
应用 HTTP 方法表达动作,路径保持资源名词,更易缓存与约定。(合理即可)