API 设计与分页 · 后端开发
什么样的 API 好用,又能放心重试?
课程目录为什么需要它
API 是对所有基于它开发的人的承诺,一旦有人依赖它,就很难再改。几条约定能让它变得可预测:清楚的资源 URL、名副其实的方法、分页返回的结果,以及网络抖动时能放心再发一次的请求。
直觉理解
资源是名词;方法是动词。
| 请求 | 意思 |
|---|---|
GET /orders | 列出订单 |
POST /orders | 创建一个订单 |
GET /orders/42 | 读取订单 42 |
PUT /orders/42 | 整个替换订单 42 |
PATCH /orders/42 | 修改部分字段 |
DELETE /orders/42 | 删除它 |
不要写成 /getOrders 或 /deleteOrder?id=42。
幂等的意思是:做两次和做一次效果相同。GET、PUT、DELETE 是幂等的:把订单 42 删两次,它还是被删了。POST 不是:发两次可能就建了两个订单。网络超时时,客户端没法知道 POST 到底有没有到达,所以支付类 API 接受一个 Idempotency-Key 请求头:服务器记住这个 key,重复的请求返回同样的结果。
分页。 永远不要一次返回一百万行。
- 偏移分页:
?page=3&size=20跳过 40 行。简单,但有新数据插入时行会错位。 - 游标分页:
?after=140&limit=20表示「id 140 之后的 20 条」。稳定又快;响应里告诉你从哪里继续:"next": 160。
定义
REST
A style of API where URLs name resources and HTTP methods say what to do with them.一种 API 风格:URL 表示资源,HTTP 方法表示对资源做什么。
幂等
Repeating the request has the same effect as sending it once. GET, PUT, DELETE are; POST isn't.重复请求和只发一次效果相同。GET、PUT、DELETE 是幂等的;POST 不是。
游标分页
Asking for items after a known position (often an id), with the response saying where to continue.请求某个已知位置(通常是 id)之后的数据,响应里告诉你下次从哪里继续。
示例
偏移分页
动手试试page 和 size 变成一个切片。
ITEMS = [{"id": i, "name": f"item{i}"} for i in range(1, 8)]
def page_of(items, page, size):
start = (page - 1) * size
chunk = items[start:start + size]
return {"items": [it["id"] for it in chunk], "page": page, "has_more": start + size < len(items)}
print(page_of(ITEMS, 1, 3))
print(page_of(ITEMS, 3, 3)){'items': [1, 2, 3], 'page': 1, 'has_more': True}
{'items': [7], 'page': 3, 'has_more': False}
- L4页码从 1 开始,所以第 1 页从 0 开始。
- L6告诉客户端还要不要请求下一页。
- L9第 3 页从 6 开始:只剩第 7 项。
幂等键
动手试试带同一个 key 的重复支付,直接用记下的结果回答,不会再扣一次钱。
CHARGES = []
SEEN = {}
def pay(key, amount):
if key in SEEN:
return SEEN[key]
CHARGES.append(amount)
result = (201, {"charge": len(CHARGES), "amount": amount})
SEEN[key] = result
return result
print(pay("k-1", 30))
print(pay("k-1", 30))
print(pay("k-2", 30))
print(CHARGES)(201, {'charge': 1, 'amount': 30})
(201, {'charge': 1, 'amount': 30})
(201, {'charge': 2, 'amount': 30})
[30, 30]
- L5以前见过这个 key?返回存下的答案。
- L13超时后的重试:同样的 key,同样的答案,不会再扣一次。
- L15两笔真实扣款:每个 key 一笔。
[30, 30]
常见错误
URL 里写动词
POST /createUser、GET /deleteUser?id=3 让人看不出在做什么,还会破坏缓存。用 POST /users 和 DELETE /users/3。
用 GET 修改数据
浏览器、爬虫和链接预览会随意发送 GET 请求。会删除或付款的 GET,迟早会被意外触发。
盲目重试 POST
超时后,第一个请求可能已经成功了。没有幂等键,重试可能造成重复的订单或扣款。
小测验
1. 哪个请求不是幂等的?
查看答案
答案:A
每次 POST 都可能新建一个订单;重复 DELETE 或 PUT 最终状态不变。
2. 哪种 URL 设计符合 REST?
查看答案
答案:A
路径里是名词,动词放在方法里。
动手练习
3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。
- 预测输出翻页
准确写出它打印的内容。
- 改需求让 DELETE 幂等
删除一个订单两次应该没有坏处:第二次也应该返回
204(已经没有东西可删,最终状态一样)。现在它会因为 KeyError 崩溃。修好delete_order,让每次调用都返回(204, None)。 - 从零写游标分页
写一个
page_after(items, after, limit)。items是按"id"排好序的字典。返回一个字典:"items":id 大于after的项里,最多limit个的 id(第一页时after是None)"next":如果后面可能还有,就是返回的最后一个 id,否则是None
page_after(ITEMS, None, 3) # {'items': [1, 2, 3], 'next': 3} page_after(ITEMS, 3, 3) # {'items': [4, 5, 6], 'next': 6} page_after(ITEMS, 6, 3) # {'items': [7], 'next': None}
本课术语
| 术语 | 中文 | 英文定义 |
|---|---|---|
| REST | REST | URLs name resources; HTTP methods say what to do. |
| Idempotent | 幂等 | Doing it twice has the same effect as once. |
| Pagination | 分页 | Returning a big list in pages. |