接口设计原则

接口设计这件事,说白了就是给后端服务画张地图。画得好,前端开发看得明白,第三方接得顺手,运维排障也轻松。画得不好……嗯,我以前接手过一个系统,同样是创建资源,有的接口用 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 在网关日志和历史版本之间串起了完整的请求链路。

下面这张图,总结了我们这一章的核心脉络。

Secrets Manager 接口设计核心原则 RESTful 准则 面向资源 · HTTP 方法语义 资源命名规范 复数 · 小写 · 层级斜杠 统一返回值 code + message + data 细则 • 路径用名词复数:/secrets /versions • 子资源用嵌套表达:/secrets/{id}/versions/{vid} • GET 不改变状态 • 一致错误格式 + request_id 可读 · 一致 · 可扩展的接口 降低联调成本 · 提升运维效率

回到最开始那句话——接口设计是给后端服务画地图。RESTful 准则是方向,资源命名是路标,统一返回值是路况播报。三条做好了,你的 Secrets Manager 不管谁来接,都能跑得顺畅。

嗯,下一部分内容就交给后面的章节去展开了。