HTTP-REST硬语义与资源建模
第一课时
REST与HTTP硬语义打底
- 新建目录:project/api_design/(后面专门放接口设计稿)
学习目标
-
能清晰解释并正确使用:HTTP 方法、幂等性、状态码语义
-
能把一个需求翻译成“资源模型 + REST 路由设计”
-
产出一份可复用的《API 设计备忘录》(后面每天都用得上)
概念学习
1.1 HTTP 方法与幂等(重点)
-
首先了解一下三个核心概念:资源、状态、幂等
-
资源(Resource):HTTP 世界里你操作的是“资源”,通常用 URL 表示:
/users/123: 用户 123 这个资源
/orders/2026-0001: 订单 2026-0001
/cart/items: 购物车条目集合(集合也是资源)
-
状态(State)
资源“现在长什么样/处于什么状态”,比如订单是 PAID 还是 CREATED。
-
幂等(Idempotent)
同一个请求重复执行 N 次,最终系统状态与执行 1 次相同(结果可能一样,也可能不完全一样,但“资源状态”不能被越改越离谱)。
幂等关心的是 服务端状态,不是你看到的返回体是否一模一样。
幂等非常现实:网络超时、前端重试、网关重发、用户疯狂连点按钮……都会让“同一请求被重复发送”。
一个很直观的判断句: “重复发一次,会不会多创建一个东西 / 多扣一次钱 / 多加一次库存 /多登陆一次?” 会 → 通常非幂等;不会 → 幂等。
-
-
GET:读资源(不改状态)
- 语义:只读取,不应该改资源状态。
- 幂等性:是(反复 GET 不会把资源越 GET 越变)。
-
安全性(safe):是(safe 指“理论上不产生副作用”)。
-
例子:
GET /users/123 → 返回用户信息
GET /orders/2026-0001 → 查订单详情
GET /products?keyword=honor → 搜索商品列表
-
注意:
有些系统会在 GET 时做“记录访问次数/埋点/更新 last_seen”,这就有副作用了。
这类副作用一般被认为是“可接受的副作用”,
但设计上尽量别让 GET 去做关键状态变更(比如扣费、下单)。
-
POST:创建/触发动作(通常非幂等)
- 语义:提交一个“要处理的东西”到某个集合资源,或触发一个动作。
-
幂等性:通常否(重复 POST 往往会多创建一次 / 多触发一次)。
-
典型创建(非幂等)
POST /orders body: {items:[...]} 第一次:创建订单 2026-0001 第二次(同样 body 又发一遍):可能创建 2026-0002 状态不同了 → 非幂等 -
典型动作(更非幂等)
POST /payments/charge body: {order_id:..., amount:...}
重复一次可能多扣一次钱 这就是为什么支付系统对幂等特别敏感。
那 POST 就不能幂等吗?能,但要你“设计幂等”
最常见办法:幂等键 Idempotency-Key(或你自己提供客户端请求 ID)
客户端发:POST /orders,带 header:Idempotency-Key: 8f3a...
服务端:如果发现这个 key 已处理过,直接返回第一次的结果,不再创建新订单
这招在 Stripe 之类支付/订单系统里非常常见(理念就是这样)。
-
PUT:整体替换(幂等)
- 语义:对某个明确资源做“整体替换”。
-
幂等性:是(你重复替换成同一个样子,最终还是那个样子)。
-
例子(用户资料整体替换):
PUT /users/123 body: {"name":"Jia", "email":"a@b.com", "age":25} 重复 N 次,最终 /users/123 都是这套字段值 → 幂等 -
注意两个关键点:
-
PUT 通常是对“已知资源 ID”:/users/123
-
整体替换意味着:没给的字段可能会被清空/重置(看实现,但语义上是替换)
一个特别直观的类比: PUT 像“覆盖保存文件”——你把文件内容覆盖成你给的内容。 - PATCH:部分更新(通常按设计可幂等) - 语义:对资源做“局部修改”。 - 幂等性:取决于你怎么定义“修改”。
-
-
幂等的 PATCH(推荐)
在 PATCH 里表达“把字段设置成某个值”:
PATCH /users/123
body: {"nickname":"猫大人"}
重复 N 次,nickname 都是“猫大人” → 幂等
-
非幂等的 PATCH(常见坑)
你在 PATCH 里表达“增量操作”:
PATCH /cart/123 body: { "add_item": {"sku":"A", "qty":1} }
发一次 qty +1
再发一次 qty 又 +1
状态变了 → 非幂等 所以: “set/replace”风格 → 更容易幂等 “increment/add/append”风格 → 往往非幂等(除非你设计去重逻辑)
-
DELETE:删除(幂等,重复删结果一致)
- 语义:删除一个资源。
-
幂等性:是(删一次资源没了;再删一次资源还是没了)。
-
例子:
DELETE /orders/2026-0001
第一次:删除成功(可能返回 200 或 204)
第二次:可能返回 404 Not Found(因为已经不存在了)
但这不影响它“幂等”:最终状态都一样——资源不存在。
1.2 状态码语义(今天先覆盖最常用的一套)
200 OK:成功(读/改都可用)
201 Created:创建成功(通常配 Location)
204 No Content:成功但无返回体(常用于 DELETE/PUT)
400 Bad Request:参数/格式错误(客户端错)
401 Unauthorized:没登录/没 token
403 Forbidden:登录了但没权限
404 Not Found:资源不存在(也常用于“隐藏存在性”)
409 Conflict:冲突(重复创建、版本冲突等)
422 Unprocessable Entity:语义校验失败(字段格式对但不合法)
429 Too Many Requests:限流
500/502/503:服务端问题(区分自身/网关/不可用)
实际操作
-
先说一下这一节课的目标哈 上面我们先了解一下主要的操作和状态码 接下来我们需要的实践并不是直接就上手敲代码设计后端了 而是我希望养成一个好的工程思维。
因此我们第一节可主要是写一份接口笔记,达到公司可以评审的api接口文档,这是一个公司团队需要做的事情。
1.1 场景:Todo(带用户)
2.1 资源模型(必须写在 api_design/todo_v1.md)
- User
- TodoList(清单)
- TodoItem(事项)
写清每个资源最少字段(例):
User: id, email, created_at
TodoList: id, user_id, name, created_at
TodoItem: id, list_id, title, done, due_date, created_at, updated_at
2.2 路由设计(REST 风格)
至少包含:
- POST /v1/auth/login(登录动作,不是资源也可以)
- GET /v1/lists
- POST /v1/lists
- GET /v1/lists/{list_id}
- PATCH /v1/lists/{list_id}
- DELETE /v1/lists/{list_id}
- GET /v1/lists/{list_id}/items
- POST /v1/lists/{list_id}/items
- PATCH /v1/items/{item_id}(跨层直接定位 item)
- DELETE /v1/items/{item_id}
2.3每个接口都要写(最少写 6 个接口的完整细节)
对每个接口写清:
方法 + 路径
请求头(是否需要 Authorization: Bearer <token>)
Query 参数(分页:page,size 或 cursor,limit 二选一)
Request body(JSON 示例)
Response body(JSON 示例)
成功状态码与失败状态码(列出原因)
要求:至少有一个接口体现 201 + Location,至少一个体现 409/422 的使用理由。
2.4幂等性设计(核心)
在文档里回答这两条(写成“规则”):
哪些接口幂等?为什么?
如果要让 POST /v1/lists 也支持幂等,你会怎么做?
提示:Idempotency-Key(请求头)+ 服务端记录请求结果
如上便是这次的实际操作任务,考虑到大多数人应该是第一次接触,那我这一次会带着大家一点点写一下这个
分步教学
在开始前,我们先理解一下接口文档的本质是什么,很多人感觉就是写一写路径就完事了,其实这个需要做的是三件事: - 资源模型:我们需要明确的写出系统里面有那些对象比如user,item等等,他们怎么关联,最少的字段有什么 - HTTP语义:来表达什么意图 - 可预测的契约:请求响应长啥样,成功失败的状态码,前端如何重试
ok接下来我们第一步先搭建骨架
# Todo API v1 设计文档
## 0. 总览
## 1. 通用约定(认证、时间、错误格式、分页)
## 2. 资源模型(User / TodoList / TodoItem)
## 3. 路由设计(REST)
## 4. 端点详细说明(至少 6 个端点写全)
## 5. 幂等性设计规则(重点)
第二步 通用约定
这一步是很多人懒得写的地方,但我认为接口文档是给团队看的,要做的是清晰明了,所以这一步我们要统一口径。写作格式主要是规则+理由+例子,大家一眼可以看懂
-
API基本信息:base url和版本策略
- 应该写什么(规则):API 统一前缀:/v1 版本升级策略:破坏性变更才升大版本(v1→v2)
- 理由:公司里多人协作时,版本策略不写清楚,客户端不知道什么时候要改
- todo例子: Base path:/v1 示例:GET /v1/lists
-
数据格式:Content-Type 与编码
-
规则:
-
请求/响应 body 默认 JSON
-
请求必须带:Content-Type: application/json; charset=utf-8(有 body 时)
-
响应带:Content-Type: application/json
-
-
理由:避免有人用表单、有人用 text,导致后端解析不一致
- 例子:请求头:
http Content-Type: application/json; charset=utf-8 Accept: application/json -
-
认证和权限:Authorization+401/403口径
-
规则:
-
除 /v1/auth/login 外,其他接口都需要:Authorization: Bearer
-
没 token / token 无效:401 Unauthorized
-
token 有,但访问别人的资源:403 Forbidden
-
(可选安全策略)也可以用 404 隐藏资源存在性,但必须写成全局规则并全站一致
-
-
理由:防止前端不知道什么时候跳登陆,保证安全审计
-
例子(俩失败案例):
- 缺token
http GET /v1/lists → 401- 访问他人list:
http GET /v1/lists → 401
-
-
统一错误返回格式
- 规则:所有的非2xx都返回统一结构,例如:
{
"error": {
"code": "validation_error",
"message": "name is required",
"details": { "field": "name" }
},
"request_id": "req_abc123"
}