Week 5 RESTful Client 这周的内容跟上周有很多是一样的,进阶讲了REST 的设计和REST的客户端。怎么把 API 设计对”到“怎么把 API 用对”的完整思维框架。Assignment2这周发布也是,主要是关于RESTful API的作业
6大约束RESTful
Week4 有提到,这里在做一些额外补充, Client–Server、Uniform Interface、Stateless、Caching、Layered System、Code on demand(可选)。如果前 5 条你都满足,基本可以称自己“RESTful”。这不是教条,而是为了解耦和规模化(扩展性、缓存、演进)而制定的一套约束。
Uniform Interface
核心是:资源有统一、可预测的表现形式,并且是“带链接的表现(Hypermedia)”——除了数据,还给出“下一步能干什么”(HATEOAS)。这样 API 能“解释”,开发者不必翻厚文档才能走通流程。
无状态(Stateless)不是“没有会话”,而是区分两类“状态”
- 资源状态(Resource state):在服务器端统一管理(比如 GTFS 中的
routes.txt、trips.txt数据)。 - 应用状态(Application state):属于客户端会话流程(比如“已选定 agency、当前分页位置、搜索关键字”),应由客户端随请求一起带上,而不是让服务器替你“记住”。因此每个请求都自描述(包含必要上下文:路径、查询参数、首部、体)。
工程要点:
- 认证/授权可用 token(放在
Authorization头里),这不违背 Stateless,因为 token 只是让服务器校验你是谁,不是服务器替你保存应用状态。 - 不要用服务器端 Session 来记住“当前 agency”,而是让客户端每次调用明确 agency 或者把“当前 agency”写入你自己的用户表后由后端查询,但请求必须可重放并自包含。
Ass2注意的点 :你的“当前使用的 bus agency”可以通过显式切换端点设置(例如 PUT /settings/agency),查询端点在服务端根据用户配置返回对应数据;也可以让所有查询都带 ?agency=GSBC_01。两种都 OK,但都遵守“请求自描述”。
缓存(Caching)设计要“能判定是否该工作”
- Why:REST 强调“无状态”,请求会“更啰嗦”;为避免过度聊天,必须协同缓存——客户端在请求里带“护栏条款”,让服务端能快速判断“是否有必要干活”。典型首部:
If-Modified-Since/If-None-Match(ETag)。 - 服务端要明确缓存策略:
Cache-Control(max-age、no-store、must-revalidate…)、ETag/Last-Modified的生成与对比逻辑。
Ass2 注意点:
- 抓取 GTFS 压缩包可以本地缓存:记录上次 ETag/时间戳,再次拉取时用
If-None-Match/If-Modified-Since,收到304 Not Modified就不重下、也不重导。 - 对你自家 API:例如“路线列表”是你数据库里的静态快照(直到下次导入),可以设置
ETag为一版号(如 agency+导入时间),列表查询返回Cache-Control: public, max-age=300。
Richardson Maturity Model
成熟度模型(RMM)指的是“你有多 REST”

Level 0 使用HTTP作为传输方式
- 只是将HTTP用作远程交互手段,而不是引入任何web机制,相当于把HTTP应用层协议降级为传输层协议,参数放在body中,以POST方式提交到某个URI上的公开服务端点
- 例子: 在一个医院挂号系统中,医院会通过某个URI来暴露出该挂号服务端点(Service Endpoint)。然后患者会向该URI发送一个文档作为请求,文档中包含了请求的所有细节
请求 某天医生的出诊信息
POST /appointmentService HTTP/1.1
[header Information]
<openSlotRequest date = "2010-01-04" doctor = "mjones" />响应 传回一个包含了所有信息文档
HTTP/1.1 200 OK
[header Information]
<openSlotList>
<slot start = "1400" end = "1450">
<doctor id = "mjones" />
</slot>有了这些信息,下一步就可以创建一个预约,同样可以通过这样向某一个断点发送一个文档来完成
Leve 1 引入了资源
- 每一个资源都有对应的标识符和表达,不是将所有请求发送到单个服务器端点,而是和单独的资源进行交互。
- 例子: 在一个医院的挂号系统中,对指定的医生会有一个对应的资源
请求:某天医生(resources)的出诊信息
POST /document/mjones HTTP/1.1
[header information]
<openSlotRequest date = "2010-01-04" />响应: 包含了各个时间段的就诊信息,这些就诊时间可以作为资源单独处理
HTTP/1.1 200 OK
[Header information]
<openSlotList>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450" />
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650" />
</openSlotList>有了这些资源后,创建一个预约就是向某个特定的就诊时间发送请求
请求:创建某一时段(Resources)的预约
POST /slots/1234 HTTP/1.1
[header information]
<appointmentRequest>
<patient id = "jsmith" />
</appointmentRequest>响应:成功预约
HTTP/1.1 200 OK
[header information]
<appointmentRequest>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450" />
<patient id = "jsmith"/>
</appointment>Level 2 HTTP动词 HTTP响应码
- HTTP谓词和资源结合起来就可以准确的表示“对XX(资源)做XX(操作)”,Server端通过HTTP状态码告诉客户端请求执行的结果,减少了服务器和客户端的耦合。
- 例子:在医院挂号系统中,获取医生的就诊时间信息需要使用GET(安全且幂等操作。
请求: GET医生的就诊时间信息
GET /doctors/mjones/slots?date=20100104&status=open HTTP/1.1
Host: royalhope.nhs.uk响应: 同level1
HTTP/1.1 200 OK
[header information]
<openSlotList>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450" />
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650" />
</popenSlotList>请求: POST创建一个预约同level1
POST /slots/1234 HTTP/1.1
[header information]
<appointmentRequest>
<patient id = "jsmith" />
</appointmentRequest>响应: 返回201 预约成功
HTTP/1.1 201 Created
Location: slots/1234/appointment
[header information]
<appointmentRequest>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450" />
<patient id = "jsmith"/>
</appointment>Level 3 Hypermedia control 超媒体控制
- HATEOAS是Hypertext As The Engine Of Application State的缩写,是指在资源的表达中包含了链接信息,客户端可以根据链接来发现可以执行的动作
请求: GET医生的就诊时间信息 (同level2)
GET /doctors/mjones/slots?date=20100104&status=open HTTP/1.1
Host: royalhope.nhs.uk响应: 每个就诊时间信息现在都包含一个URI, 用来告诉我们如何创建一个预约
Hypermedia control的关键就是在:它告诉我们下一步能够做什么,以及对应相关资源的URI
HTTP/1.1 200 OK
[header information]
<openSlotList>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450" />
<link rel = "/linkrels/slot/book" uri = "/slots/1234" />
</slot>
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650" />
<link rel = "/linkrels/slot/book" uri = "/slots/5678" />
</slot>
</openSlotList>请求: POST创建一个预约(同level1)
POST /slots/1234 HTTP/1.1
[header information]
<appointmentRequest>
<patient id = "jsmith" />
</appointmentRequest>响应:包含了一系列的超媒体控制,用来告我们后面可以进行什么操作。
HTTP/1.1 201 Created
Location: slots/1234/appointment
[header information]
<appointmentRequest>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450" />
<patient id = "jsmith"/>
<link rel = "/linkrels/appointment/cancel" uri = "/slots/1234/appointment"/>
<link rel = "/linkrels/appointment/addTest" uri = "/slots/1234/tests"/>
<link rel = "self" uri = "/slots/1234/appointment"/>
<link rel = "/linkrels/appointment/changeTime" uri = "/doctors/mjones/slots?date=20100104@status=open"/>
<link rel = "/linkrels/appointment/updateContactInfo" uri = "/patients/jsmith/contactInfo"/>
<link rel = "/linkrels/help" uri = "/help/appointment"/>
</appointment>Ass2注意点 Level 0:一个大入口;Level 1:资源 URI;Level 2:正确用 HTTP 动词与状态码语义化资源+动词+状态码);Level 3:HATEOAS(响应里有“下一步”链接)。目标是至少到 Level 2,力所能及往 Level 3 靠。
RESTful 应用场景
Resource(资源):一切对外暴露的东西都用 URI 标识,比如 /orders/1234、/payments/1234。
Representation(表现层):资源用 JSON/XML 表示(图里用 XML),请求/响应里来回传。
Stateless(无状态):服务器不记你是谁、对话进行到哪一步;每个请求都自带完成所需的信息(token、body、Accept/Content-Type 等)。
Uniform Interface(统一接口):少量 HTTP 动词完成大部分事:GET/POST/PUT/PATCH/DELETE/OPTIONS/HEAD。
HATEOAS(Hypermedia as the Engine of Application State):响应里给下一步可做的链接(<link rel="payment" href="...">),客户端“跟着链接走”。
- 顾客工作流:
Place Order → Paid → Drink Received,中途可Update(比如加一份 Espresso)。

- 咖啡师工作流:
Lookup Next Order → Drink Made → Take Payment → Drink Released。 - 每个状态变迁 = 对某个 URI 用某个 HTTP 方法。

下单

- 动作:
POST /orders - 为什么用 POST:创建新的订单资源(服务器分配 ID)。
- 期望响应:
201 Created+Location: /order/1234告诉新资源的地址(URI)。 + 表现体里带 links(响应体里给出后续可操作的关系链接(HATEOAS),付款的链接。)。
我还能改单吗?

- 动作:
OPTIONS /order/1234 - 意义:询问服务器该资源当前允许的动词(
Allow: GET, PUT)OPTIONS、Allow header- 如果还可以
PUT,说明订单仍处于“可修改”状态(尚未制作/出杯/结束)。
- 如果还可以
更新订单

动作:PUT /order/1234(把订单更新为“加一份 shot”)
- PUT = Idempotent(幂等):同样的 PUT 重试 10 次,最终状态仍然一致,不会“越更越多”。
- 与 PATCH 的区别:PUT 通常是整体替换(或显式“设为”某状态),PATCH 是部分修改。
更新成功返回
200 OK(或无体时204 No Content),并可在体内继续返回 links(例如付款)。
并发冲突
咖啡师在修改订单之前把饮品做出来了,会有两种情况


- 场景:你在 PUT 之前,咖啡师可能已经把饮品做了,资源状态先一步改变。
- 结果:服务器返回
409 Conflict,提示你当前变更与资源现状冲突。 - 另一种结果:去option
OPTIONS /order/1234看现在还能干啥(只允许GET),然后GET拉取最新状态,跟着链接走(可能进入付款或取餐)。
使用 ETag / If-Match(Optimistic Concurrency, 乐观锁)
GET /order/1234→ 响应头ETag: "abc123"PUT /order/1234+ 头If-Match: "abc123"- 若期间资源变了,返回
412 Precondition Failed,避免“丢失更新”。
订单更新成功

- 动作:
GET /order/1234 - 作用:拿到最新表示(representation),里面含有
rel="payment"的链接。 - 结果:Follow the links(沿着超媒体链接推进状态),而不是在客户端硬编码流程。
付款

- 动作:
PUT /payment/order/1234 - 为什么用PUT:幂等——网络抖动可能会导致客户重新付款,所以用PUT不会多扣款(在真实系统里再配合幂等键 idempotency key)。
- 期望响应:
201 Created(或200/204取决于你把付款建模为“创建一个 payment 子资源”还是“设置支付状态已完成”)。
- 很多系统会把付款单独建模成
/orders/1234/payment或/payments/{paymentId};- 无论路径如何,关键是让重试安全(idempotent)。
确认支付状态

- 动作:
GET /payment/order/1234 - 期望响应:
200 OK+ 支付详情(cardNo、name、amount ……)。 - 下一步:HATEOAS 可再给出“可取餐”的链接或当前队列位置。
REST 客户端
一个合格的客户端开发流程是:读文档→识别对象→拼地址→处理动作(过滤、编辑、删除)
URI Design
- Avoid using ‘www’, instead:避免使用www, 在URL中多使用操作名字e.g., /getCoffeeOrders, /createOrder, /getOrder?id=123
- 用名词复数:
/routes、/stops/{id};关系用层级或查询表达:/routes/{id}/tripsvs/trips?routeId=...,以“预期返回的资源”来选方案。- • Walmart /items /items/{id}
- • LInkedIn /people /people/{id}
- • BBC /programmes /programmes/{id}
- 查询字符串用于过滤/选择:
/routes?short_name=333&direction=outbound。- • /orders?date=2015-04-15
- • /customers?state=NSW&status=gold
- • Twitter /friendships/lookup
- • Walmart /search?query
- 资源关系的表达
- • Facebook /me/photos
- • Walmart /items/{id}/reviews
- • /customers/123/orders vs. /orders?customer=123
- 状态码要一致:
GET 200、POST 201(+Location)、PUT 200/204/201、DELETE 200/204,不要乱用400一把梭;404、403、401各有场景。

- 响应格式:单个对象返回对象,多个返回集合容器(建议统一
{ items: [...], total, _links }),并支持内容协商(至少 JSON)

具体的到实例可以长下面这样子
- GET /coffeeOrders/123

- POST /coffeeOrders

- PUT /coffeeOrders/123
- PATCH /coffeeOrders/123

- DELETE /coffeeOrders/123

Consuming a REST API
The client in this situation is pre-written software program 客户端是预先编写好的程序

两类客户端形态:
- 传统 Web 应用(浏览器拿 HTML):整页渲染为主;
- REST 客户端(程序拿 JSON/XML):通过 HTTP 动词直接与 API 交互,可嵌入到应用中作为“数据层”。
应用里常常内置一个 REST client去调用外部服务;交互是纯 HTTP、stateless的。
SPA(Single Page Application)场景:前端只刷新局部 UI、与 API 频繁交换 JSON,提高交互性;而“人机交互”主要发生在 UI,不在 API。
How to write Web REST API Client
首先就是阅读API 文档 / OpenAPI(=Swagger) 了解
- 对象模型(返回 JSON 里有哪些字段/链接)
- 地址构造(资源的 URI 规则、查询参数)
- 可做的动作(过滤、创建、更新、删除)
核心任务就是
- Recognising the Objects in HTTP responses
- Constructing Addresses (URLs) for interacting with the service
- Handling Actions such as filtering, editing or deleting data
Client-API interaction pattern
循环模式(very basic client pattern)
发送 HTTP 请求 → 2) 把响应(JSON)暂存 → 3) 解析出当前上下文(要显示什么、下一步能做什么) → 4) 渲染 UI(含动作按钮);然后根据用户操作或链接再走下一轮。
Python requests
Requests will allow you to send HTTP/1.1 requests using Python. It also allows you to access the response data of Python in the same way.
json= 参数会自动 json.dumps() 并设置 Content-Type: application/json;
适合自动化脚本/服务对服务(machine-to-machine)
import requests
resp = requests.get('https://todolist.example.com/tasks/')
if resp.status_code != 200:
# This means something went wrong
raise ApiError('GET /task/ {}'.format(resp.status_code))
for todo_item in resp.json():
print('{}{}'.format(todo_item['id'], todo_item['summary']))
"""
the json argument to post. If we use that, requests will do the following for
us:
Convert that into a JSON representation string, json.dumps()
Set the requests’ content type to "application/json" (by adding an HTTP
header).
"""
task = {"summary": "Take out trash", "description": ""}
resp = request.post('https://todlist.example.com/task/', json=task)
if resp.status_code != 200:
raise ApiError('POST /tasks/ {}'.format(resp.status_code))
print('Created task. ID: {}'.format(resp.json()['id']))正确校验状态码(Status Code Check)
- GET 成功通常 200;
- POST(创建) 期望 201 Created(并常带
Location); - PUT/PATCH/DELETE 成功常用 200/204;
- 失败分 4xx(客户端问题) 与 5xx(服务端问题),响应体应包含可读错误。 → 客户端要按“操作语义”检查返回码,否则容易把错误当成功用
Caching API Requests
通过导入import requests_cache上,减少重复请求、加快体验;也支持 dict/redis/mongodb。适合读多写少、且服务端响应变化没那么频繁的资源。
By default the cache is saved in a sqlite database. 默认情况下缓存是存在SQlite数据库中的
import requests_cache
requests_cache.install_cache(cache_name=‘mystuff_cache', backend='sqlite', expire_after=180)封装为“自己的 API 库”
把“单次调用”升级为“稳定可复用的库”,既方便团队,也让外部调用者容易使用你的 API
- 当项目复杂时,不要在业务代码到处散落
requests.get/post;请封装成专用 SDK/库,统一:- base URL / 认证 / 超时 / 重试 / 错误映射 / 日志 / 追踪 id;
- 公共能力(分页、批量、幂等键、重放保护)
import requests, time
class Api:
def __init__(self, base, token=None, timeout=10):
self.base, self.timeout = base.rstrip('/'), timeout
self.session = requests.Session()
self.session.headers.update({'Accept':'application/json'})
if token: self.session.headers['Authorization'] = f'Bearer {token}'
def _url(self, path): return f'{self.base}{path}'
def _req(self, method, path, **kw):
for attempt in range(3): # 轻量重试(可加指数退避)
r = self.session.request(method, self._url(path), timeout=self.timeout, **kw)
if r.status_code >= 500 and attempt < 2:
time.sleep(0.5 * (2**attempt)); continue
return r
return r
def list_items(self, page=1):
r = self._req('GET', f'/items?page={page}')
r.raise_for_status(); return r.json()
def create_item(self, title, idempotency_key=None):
headers = {}
if idempotency_key: headers['Idempotency-Key'] = idempotency_key
r = self._req('POST', '/items', json={'title':title}, headers=headers)
if r.status_code != 201: raise RuntimeError(r.text)
return r.json(), r.headers.get('Location')MUST IGNORE 原则
MUST IGNORE:客户端必须忽略响应中不认识的字段,而不是报错或崩溃。这是 HTTP/HTML 演进的通用策略,也适用于自己的客户端。
V1 响应含 links/data/actions;V2 新增 extensions。旧客户端应该跳过新字段继续处理,以实现向前兼容;如果要用新能力,升级客户端即可。

发表回复
要发表评论,您必须先登录。