第3章:接口设计原则 — RESTful、资源命名、幂等性与无状态

接口设计这事儿,说白了就是给系统之间定规矩。规矩定好了,大家照着做,不出乱子。我以前带团队时经常遇到一种情况:接口设计文档写了几十页,开发还是反复来问「这个接口到底是 POST 还是 PUT?」嗯,问题就出在没把核心原则吃透。

这一章咱们就聊四个最基础、也最重要的设计原则:RESTful 风格、资源命名、幂等性、无状态。掌握了这些,你的接口起码能上得了台面。

3.1 RESTful 设计规范 — 别把 API 当 RPC 用

RESTful 不是什么高深概念。它要求你把操作对象抽象成「资源」,用 HTTP 方法表达对资源的动作。我个人习惯先画一张图,把资源、方法、状态码串起来,再动手写代码。

核心口诀:资源是名词,方法是动词。GET 查、POST 增、PUT 全量改、PATCH 局部改、DELETE 删。

举个例子:管理用户的密钥信息。

  • GET /api/keys — 获取所有密钥列表
  • GET /api/keys/{keyId} — 获取某个密钥详情
  • POST /api/keys — 创建新密钥
  • PUT /api/keys/{keyId} — 完全替换某个密钥
  • PATCH /api/keys/{keyId} — 更新密钥状态(比如禁用)
  • DELETE /api/keys/{keyId} — 删除密钥

注意:别把 CRUD 全塞进一个 POST /api/keys/doSomething 里。我见过有人把删除操作用 GET 带参数 ?action=delete 实现,这会造成缓存污染,后果很严重。

避坑:我曾经接手过一个遗留系统,所有写操作都用 POST,响应码统一返回 200。排查问题时根本分不清是「创建成功」还是「更新成功」。后来不得不花两周重构,浪费了大量时间。

3.2 资源命名 — 清晰胜过花哨

资源名称是接口的第一张名片。命名时记住几条铁律:

  1. 全小写 + 中划线:/api/user-keys 而不是 /api/UserKeys 或 /api/user_keys。
  2. 用复数名词:/api/keys 而不是 /api/key,这样语义统一,好扩展。
  3. 层级关系用斜杠:比如 /api/keys/{keyId}/permissions,一眼就能看出权限属于哪个密钥。
  4. 查询参数只用于过滤/排序/分页:GET /api/keys?status=active&page=1,不要用来表示资源身份。
正确示例 错误示例 错误原因
GET /api/keys GET /api/getKeys 动词混入了路径
POST /api/keys POST /api/addKey 动词代替了方法
GET /api/keys/123 GET /api/key?id=123 参数表示资源身份
PUT /api/keys/123/permissions PUT /api/keys/123?update=permissions 子资源未显式

你想想看,如果接口名字都不一致,前端、SDK、文档都得跟着乱。我在项目里推动过一个命名规范,要求所有新接口必须经过 review,后来维护成本至少降了 30%。

3.3 幂等性 — 重复请求不闯祸

幂等性是指:同一个请求执行一次和多次,对系统状态的影响一样。说白了,网络重试不担心。

HTTP 方法的幂等性对照表:

方法 是否幂等 说明
GET 是 多次查询结果不变(不考虑并发修改)
PUT 是 完全替换,多次效果一样
DELETE 是 第一次删除成功,后续返回 404,状态不影响
PATCH 否(默认) 增量更新可能重复执行累加
POST 否 多次创建会生成多个资源

实战技巧:如果你一定要让 POST 接口支持幂等,可以在请求头里加一个 Idempotency-Key。服务端用这个 key 去重,比如支付创建接口。我记得有家大厂因为没做幂等,用户连续点两次按钮,结果扣了两次款——事故通报直接挂了几周。

另外要注意:PATCH 不一定幂等。比如 PATCH /api/keys/123 请求体是 { "usageCount": 10 },如果你写的是「累加」逻辑而不是「覆盖」逻辑,发两次就变成 20 了。所以设计 PATCH 时建议用「覆盖式」语义,或者干脆限制 PATCH 只能操作不可累加字段。

3.4 无状态 — 别再往服务端塞 Session

RESTful 要求服务端不保存客户端上下文。每个请求必须自包含所有需要的信息(认证、目标资源、参数)。为什么?

  • 水平扩展更容易:任何一台服务器都能处理任何请求,不需要共享 session。
  • 故障恢复快:一台挂了,另一台直接接管。
  • 接口更可观测:抓到一条请求日志,就能完全还原调用场景。

常见的无状态实现方式:

  1. 用 JWT 携带用户身份和权限信息,每次请求带上 Authorization: Bearer <>。
  2. 分页、排序信息通过查询参数传递:GET /api/keys?page=2&size=20。
  3. 不要在服务端保存临时状态(比如「当前用户正在编辑的草稿」),这种应该放到客户端或数据库。

反例:我曾见过一个旧系统,用 Cookie 里的 sessionId 保存了「当前选中的密钥 ID」。用户跳转到其他页面再回来,选中状态丢了,前端还要重新查询。后来改成把选中 ID 放在 URL 片段或本地存储里,问题就解决了。

但无状态也不是的。有些场景(比如大型文件上传、长轮询)天然需要服务端维护状态。这时候你可以把这部分独立成有状态服务,而核心的业务接口依然保持无状态。原则嘛,能无状态就无状态,实在不行才破例。

接口设计核心原则关系图 RESTful 设计 资源命名规范 幂等性保证 无状态要求 HTTP 方法语义 设计要点总结 • 资源用名词复数,路径用中划线 • 幂等性由 HTTP 方法保证,必要时加 Idempotency-Key • 无状态通过 JWT/查询参数实现,便于水平扩展 • 保持统一风格,避免动词混入路径

上面这张图把四个原则串起来了。核心是 RESTful 设计,它规定了资源命名、幂等性、无状态和 HTTP 方法各自的角色。你设计接口时,可以拿这张图当 checklist。

最后提醒:别为了「标准」而牺牲实际可读性。比如内网调试场景,有时为了快速排查,我会在响应里附带一个 x-debug-message 头,这在严格 RESTful 里算「状态传递」,但团队都能接受。原则是死的,人是活的,关键是一致性——你们团队内部约定好了,就别反复改。