人人都能学编程
English 开始学习
专项 · 后端第 6 课 / 共 6 课免费阅读

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}
逐步执行
  1. L4页码从 1 开始,所以第 1 页从 0 开始。
  2. L6告诉客户端还要不要请求下一页。
  3. 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]
逐步执行
  1. L5以前见过这个 key?返回存下的答案。
  2. L13超时后的重试:同样的 key,同样的答案,不会再扣一次。
  3. L15两笔真实扣款:每个 key 一笔。[30, 30]

常见错误

URL 里写动词

POST /createUser、GET /deleteUser?id=3 让人看不出在做什么,还会破坏缓存。用 POST /users 和 DELETE /users/3。

用 GET 修改数据

浏览器、爬虫和链接预览会随意发送 GET 请求。会删除或付款的 GET,迟早会被意外触发。

盲目重试 POST

超时后,第一个请求可能已经成功了。没有幂等键,重试可能造成重复的订单或扣款。

小测验

1. 哪个请求不是幂等的?

  1. APOST /orders
  2. BDELETE /orders/7
  3. CPUT /orders/7
查看答案

答案:A

每次 POST 都可能新建一个订单;重复 DELETE 或 PUT 最终状态不变。

2. 哪种 URL 设计符合 REST?

  1. AGET /users/7/orders
  2. BGET /getOrdersForUser?id=7
  3. CPOST /users/7/listOrders
查看答案

答案:A

路径里是名词,动词放在方法里。

动手练习

3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。

  1. 预测输出翻页

    准确写出它打印的内容。

  2. 改需求让 DELETE 幂等

    删除一个订单两次应该没有坏处:第二次也应该返回 204(已经没有东西可删,最终状态一样)。现在它会因为 KeyError 崩溃。修好 delete_order,让每次调用都返回 (204, None)。

  3. 从零写游标分页

    写一个 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}

在互动课里练这一课

打开互动课,这些例子都能直接改、直接运行,练习题提交马上判题。免费开始,不用注册。

互动课按顺序解锁:前面的课还没学,会先带你到课程地图。

本课术语

术语中文英文定义
RESTRESTURLs 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.