第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 资源命名 — 清晰胜过花哨
资源名称是接口的第一张名片。命名时记住几条铁律:
- 全小写 + 中划线:
/api/user-keys而不是/api/UserKeys或/api/user_keys。 - 用复数名词:
/api/keys而不是/api/key,这样语义统一,好扩展。 - 层级关系用斜杠:比如
/api/keys/{keyId}/permissions,一眼就能看出权限属于哪个密钥。 - 查询参数只用于过滤/排序/分页:
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。
- 故障恢复快:一台挂了,另一台直接接管。
- 接口更可观测:抓到一条请求日志,就能完全还原调用场景。
常见的无状态实现方式:
- 用 JWT 携带用户身份和权限信息,每次请求带上
Authorization: Bearer <>。 - 分页、排序信息通过查询参数传递:
GET /api/keys?page=2&size=20。 - 不要在服务端保存临时状态(比如「当前用户正在编辑的草稿」),这种应该放到客户端或数据库。
反例:我曾见过一个旧系统,用 Cookie 里的 sessionId 保存了「当前选中的密钥 ID」。用户跳转到其他页面再回来,选中状态丢了,前端还要重新查询。后来改成把选中 ID 放在 URL 片段或本地存储里,问题就解决了。
但无状态也不是的。有些场景(比如大型文件上传、长轮询)天然需要服务端维护状态。这时候你可以把这部分独立成有状态服务,而核心的业务接口依然保持无状态。原则嘛,能无状态就无状态,实在不行才破例。
上面这张图把四个原则串起来了。核心是 RESTful 设计,它规定了资源命名、幂等性、无状态和 HTTP 方法各自的角色。你设计接口时,可以拿这张图当 checklist。
最后提醒:别为了「标准」而牺牲实际可读性。比如内网调试场景,有时为了快速排查,我会在响应里附带一个 x-debug-message 头,这在严格 RESTful 里算「状态传递」,但团队都能接受。原则是死的,人是活的,关键是一致性——你们团队内部约定好了,就别反复改。