资源而不是动作
REST 的核心是把「事物」当资源,用 URI 标识,用 HTTP 方法(GET/POST/PUT/PATCH/DELETE)表示操作。反例是把动作写进路径,如 /getUser、/createOrder,这把 RPC 风格混进了 REST。
常用约定
| 方法 | 含义 | 幂等 |
|---|---|---|
| GET | 读取 | 是 |
| POST | 新建 | 否 |
| PUT | 整体替换 | 是 |
| PATCH | 局部更新 | 否 |
| DELETE | 删除 | 是 |
三个实践要点
- 复数资源名:用
/users而非/user,集合与单项用/users/{id}区分; - 用状态码表达结果:201 新建成功、400 参数错误、401 未认证、404 不存在、500 服务端错误;
- 分页与过滤:列表用
?page=2&size=20,避免一次返回全量拖垮接口。
两个反模式
- 把动词当资源:与其用 POST
/users/{id}/ban,不如用一个状态字段做 PATCH; - 忽略幂等:重试 POST 可能重复创建,关键写操作要配合幂等键。
动手试试
校验接口返回的 JSON:JSON 格式化。
实战案例:三个让调用方难受的设计
- 动词塞进路径:
POST /getUserList无法利用 HTTP 语义与缓存;应改为GET /users,动作交给方法表达。 - 状态码一律 200:无论成败都返回 200、靠 body 里的业务码判断,网关与重试逻辑无法区分。失败应使用 4xx / 5xx。
- 分页只有 offset:数据持续写入时 offset 分页会漏项或重复。高频变更的列表应提供游标分页。
常见问题(FAQ)
REST 必须用名词复数吗?这是常见约定而非强制,关键是同一资源在全站使用同一种写法。PATCH 与 PUT 怎么选?PUT 表示整体替换,PATCH 表示局部更新;只改一个字段用 PATCH 更贴合语义。错误响应该包含什么?稳定的错误码、可读信息与字段级原因,别把内部堆栈暴露给调用方。一定要写 OpenAPI 文档吗?对外接口建议有,它是契约、自动化测试与 SDK 生成的基础。
幂等、并发与缓存:三个必须提前定的契约
- 幂等键:写接口(下单、支付、发起任务)应支持客户端传入
Idempotency-Key,服务端按该键去重并在有效期内返回同一结果,避免重试造成重复扣款; - 并发控制:更新接口提供
ETag+If-Match(乐观锁),冲突时返回412,比盲目覆盖安全得多; - 缓存语义:明确哪些响应可缓存、缓存多久,用
Cache-Control表达;列表类接口默认no-store,静态资源才用长缓存加哈希。
分页的两种形态
- 游标分页:
?cursor=…&limit=50,适合不断有新数据写入的列表,结果稳定且深翻页不退化; - 偏移分页:
?offset=…&limit=50,实现简单,但深翻页时数据库开销随 offset 线性增长,且插入新数据会导致漏项或重复。 - 无论哪种,响应里都应给出显式的“是否还有下一页”字段,而不是让调用方靠“返回条数小于 limit”猜测。
容易被忽略的细节
- 时间格式:统一使用 UTC 的 ISO 8601(带
Z),不要把本地时间字符串塞进接口; - 空值与缺省:明确
null与字段缺失的语义差异,并保持稳定; - 大整数:超过 2^53 的整数(如订单号)以字符串传输,避免 JSON 解析丢精度;
- 错误码稳定:错误码一旦发布就不应改义,新增语义请新增错误码。
健康检查与运维端点
- 区分存活与就绪:存活探针只判断进程是否需要重启,就绪探针判断是否可以接流量,二者混用会导致滚动更新时短暂全量不可用;
- 探针要轻:健康检查不应触发数据库全表扫描或下游调用,否则检查本身成了故障源;
- 不要暴露内部信息:健康端点只返回状态与版本,不要附带依赖地址、环境变量等敏感内容;
- 元数据端点:
/version之类端点便于排查“线上跑的是哪个版本”,请带上构建哈希而非仅版本号。
文档即契约
把接口定义(OpenAPI 等)纳入版本控制,并让它成为唯一事实来源:类型、文档与测试都从它生成。手工维护的文档一定会过期,而过期的文档比没有文档更危险——调用方会按错误的理解去实现。