接口设计原则
接口设计这件事,说白了就是给后端服务画张地图。画得好,前端开发看得明白,第三方接得顺手,运维排障也轻松。画得不好……嗯,我以前接手过一个系统,同样是创建资源,有的接口用 POST,有的用 PUT,还有的直接用了 GET 带 body。每次联调都像在猜谜。
所以这一章,我们聊聊 Secrets Manager 接口应该怎么设计。我会把个人习惯和一些踩过的坑都摊开来聊。
RESTful API 设计准则
RESTful 不是什么高深理论,它就是一套约定。你守约定了,大家省心。
核心准则:面向资源设计接口,使用标准 HTTP 方法表达操作语义。
举个例子。你要操作「密钥」这个资源,对应的接口应该是这样的:
| HTTP 方法 | 路径 | 语义 |
|---|---|---|
| GET | /secrets | 列出所有密钥 |
| POST | /secrets | 创建新密钥 |
| GET | /secrets/{id} | 获取单个密钥详情 |
| PUT | /secrets/{id} | 全量更新密钥 |
| PATCH | /secrets/{id} | 部分更新密钥 |
| DELETE | /secrets/{id} | 删除密钥 |
有人会问:我能不能用 POST 做更新?技术上当然可以。但别人一看你的接口,第一反应会是「这是个创建操作」,这就产生了认知偏差。我在项目中遇到过好几次,因为接口语义混乱导致线上故障——运维想删除密钥,结果调成了创建接口,旧的没删掉,新的倒出来一堆。
千万别干的事:用 GET 请求做数据变更。哪怕只是触发一次密钥轮转,也不要用 GET。幂等性和安全性都会出问题。
资源命名规范
命名这件事,我自己的经验是「一致性比花哨重要一百倍」。你看看很多大厂的 API,路径结构几乎都是一个模子刻出来的。
基本原则其实就几条:
- 用名词,不用动词。路径里出现动作词就是一种坏味道。
/secrets是对的,/getSecret是错的。 - 复数形式。不管是单数还是复数,选一个用到底。我习惯用复数。所以
/secrets而非/secret。 - 小写加连字符。驼峰命名在 URL 里很丑。
/secret-versions比/secretVersions可读性好得多。 - 层级关系用斜杠表达。子资源放在父资源后面。
/secrets/{id}/versions,一看就知道版本是密钥的子概念。
看看一个 Secrets Manager 的资源树设计:
# 根资源
/secrets
# 带 ID 的单一资源
/secrets/{secret_id}
# 子资源集合
/secrets/{secret_id}/versions
# 子资源单一
/secrets/{secret_id}/versions/{version_id}
# 跨资源的动作——用参数表达
/secrets/{secret_id}?action=rotate
这个结构有什么好处?我举个例子。假设你要给前端写文档,照着这个路径结构,他把斜杠理解成「的」,就能读懂——/secrets/{secret_id}/versions 就是「某个密钥的所有版本」。不需要翻两页文档才能搞明白。
一个实用技巧:如果你发现某个资源路径里出现了两个以上「/」,就该考虑是不是嵌套太深了。/secrets/{id}/versions/{vid}/audit/logs 这种写法,明显该用查询参数来展平。
统一返回值结构
这一节我要多说两句。返回值结构不统一,是接口混乱的万恶之源。我之前参与过一个项目,团队 8 个人,每个人返回的错误体都不一样。有人用 {"error": "xxx"},有人用 {"message": "xxx"},还有人只返回一个 HTTP 状态码,body 是空的。前端对接的时候,代码里全是 if-else 分支判断。
我们给 Secrets Manager 定一个统一的返回结构,大家都省心。
// 成功响应
{
"code": 0,
"message": "success",
"data": {
"secret_id": "sec-abc123",
"name": "db_password",
"created_at": "2025-01-15T10:30:00Z"
},
"request_id": "req-7a8b9c0d"
}
// 错误响应
{
"code": 40001,
"message": "密钥名称已存在",
"data": null,
"request_id": "req-7a8b9c0d"
}
这个结构我解释一下:
- code:业务状态码。0 表示成功,非 0 表示具体错误类型。别用 HTTP 状态码当业务码,4xx 和 5xx 太粗了,不够用。
- message:人类可读的信息。成功时固定 "success",错误时给具体提示。
- data:实际数据载体。成功时放业务数据,错误时放 null 或者放个错误详情对象。
- request_id:每次请求的唯一标识。排障时没有这个字段,你会在日志里翻到崩溃。
你看,这个结构不管成功还是失败,骨架是一样的。前端只用解析 code 字段就能判断流程走向,不需要关心 body 里到底塞了什么。
为什么 code 比 HTTP 状态码更好用?
假设你要区分「密钥不存在」和「密钥已过期」两种错误。HTTP 只能返回 404 和 403。但业务 code 可以定义 40401 和 40402。客户端可以根据 code 做不同的处理,而不是死磕状态码。
这里补充一个分页场景的返回值设计。列表接口经常需要分页,我们的结构长这样:
{
"code": 0,
"message": "success",
"data": {
"items": [
{ "secret_id": "sec-001", "name": "db_pass" },
{ "secret_id": "sec-002", "name": "" }
],
"total": 42,
"page": 1,
"page_size": 20
},
"request_id": "req-xyz789"
}
分页信息放在 data 里,不污染顶层结构。这样不管有没有分页,客户端的解析逻辑都是一样的——先看 code,再取 data。至于 data 里有什么,那是具体接口的事。
个人小建议:request_id 一定要在响应头里也带一份,比如 X-Request-Id。这样网络代理层也能拿到这个 ID,排障的时候链路更清晰。我记得有一次线上事故,就是靠这个 ID 在网关日志和历史版本之间串起了完整的请求链路。
下面这张图,总结了我们这一章的核心脉络。
回到最开始那句话——接口设计是给后端服务画地图。RESTful 准则是方向,资源命名是路标,统一返回值是路况播报。三条做好了,你的 Secrets Manager 不管谁来接,都能跑得顺畅。
嗯,下一部分内容就交给后面的章节去展开了。