1. 接口设计基础:RESTful API 设计原则、HTTP 协议核心要点、接口版本管理策略

各位同学,咱们今天聊接口设计。说实话,我见过太多项目因为接口设计没想清楚,后期改得痛不欲生。我自己就踩过不少坑,今天把这些经验掰开揉碎了讲给你们听。

1.1 RESTful API 设计原则——不只是“约定”

RESTful 不是什么高深理论,说白了就是一套让前后端好好沟通的规矩。我刚开始做接口时,也犯过“接口命名随心所欲”的毛病,后来被前端同事追着骂……嗯,从那以后我就老实了。

1.1.1 资源导向的 URL 设计

核心思想就一句话:URL 只表示资源,不表示动作。动作交给 HTTP 方法去表达。

✅ 正确示例:

  • GET /users — 获取用户列表
  • POST /users — 创建用户
  • GET /users/{id} — 获取单个用户
  • PUT /users/{id} — 全量更新用户
  • PATCH /users/{id} — 部分更新用户
  • DELETE /users/{id} — 删除用户

❌ 错误示例(我曾经在项目里见过):

  • GET /getUserList — 动词混入 URL
  • POST /deleteUser — 用 POST 做删除
  • GET /users/delete/123 — 路径里写动作

这些写法,说白了就是没想清楚“资源”是什么。

1.1.2 集合与单数资源的层级关系

资源之间有从属关系时,用路径层级表达:

GET /users/{userId}/orders          — 用户的订单列表
GET /users/{userId}/orders/{orderId} — 用户的某个订单
POST /users/{userId}/orders          — 为用户创建订单

我个人习惯,嵌套层级不超过三层。超过三层,要么是设计有问题,要么该考虑用查询参数了。

1.1.3 查询参数用于过滤、排序、分页

别把过滤条件塞进路径里。举个例子:

# 好的做法
GET /orders?status=paid&page=1&size=20&sort=created_at,desc

# 坏的做法
GET /orders/status/paid/page/1/size/20

为什么?因为查询参数天然支持可选、组合、扩展。路径参数搞这么复杂,前端调用时拼接都费劲。

1.1.4 使用标准 HTTP 状态码

很多团队喜欢自定义状态码,比如 code: 10001 表示“用户不存在”。我个人建议:先用好 HTTP 标准状态码,再考虑业务码。

场景 HTTP 状态码 说明
获取成功 200 OK 标准响应
创建成功 201 Created 通常返回 Location 头
请求参数错误 400 Bad Request 比如缺少必填字段
未认证 401 Unauthorized 没带 token 或 token 过期
无权限 403 Forbidden 认证了但没权限
资源不存在 404 Not Found 路径或资源 ID 错误
服务端异常 500 Internal Server Error 别把堆栈信息返回给客户端

避坑指南:我曾经见过一个项目,所有成功响应都返回 200,错误也返回 200 然后在 body 里放 code: -1。前端每次都要先判断 code 再决定是否解析数据。这种设计,说白了就是让调用方多写一堆 if-else。能用 HTTP 状态码表达的,就别自己造轮子。

1.2 HTTP 协议核心要点——你每天都在用,但真的懂吗?

HTTP 协议是接口的底层通信语言。很多性能问题、实时性问题,追根溯源都是对 HTTP 理解不够深。

1.2.1 请求方法与幂等性

幂等性这个概念,面试常考,实际开发也容易翻车。

  • GET:幂等,安全。多次请求结果一样,不改变资源状态。
  • PUT:幂等。多次全量更新同一个资源,最终状态一致。
  • DELETE:幂等。第一次删除成功,第二次返回 404,但资源状态不变。
  • POST:非幂等。每次调用都可能创建新资源。
  • PATCH:非幂等(严格来说)。部分更新可能依赖当前状态。

注意:我遇到过有人用 POST 做更新操作,结果前端因为网络抖动重复提交,订单下了两遍。这就是没理解幂等性的后果。更新操作,能用 PUT 或 PATCH 就别用 POST。

1.2.2 缓存控制——别让浏览器白干活

HTTP 缓存是提升接口响应速度的利器,但很多人要么不用,要么乱用。

# 强缓存
Cache-Control: max-age=3600          # 1小时内不请求服务器
Expires: Wed, 21 Oct 2025 07:28:00 GMT

# 协商缓存
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

我个人习惯:静态资源用强缓存,动态接口用协商缓存或干脆不缓存。比如用户信息接口,缓存 5 秒就够了,别设成 1 小时——用户改了头像,你让他等一小时才看到?

1.2.3 内容协商与 Accept 头

同一个接口,客户端想要 JSON,另一个客户端想要 XML,怎么办?用内容协商。

# 客户端请求
GET /users/123
Accept: application/json

# 服务端响应
Content-Type: application/json; charset=utf-8

如果服务端不支持客户端请求的格式,返回 406 Not Acceptable。这个机制在微服务间通信时特别有用,我曾在项目中用这个实现了一套接口同时支持 JSON 和 Protobuf 序列化。

1.2.4 连接管理——Keep-Alive 与队头阻塞

HTTP/1.1 默认开启 Keep-Alive,复用 TCP 连接。但有个老问题:队头阻塞。

什么意思?就是同一个连接上,前一个请求没响应,后面的请求就得等着。HTTP/2 用多路复用解决了这个问题,但很多项目还在用 HTTP/1.1。

实战建议:如果接口并发高,考虑升级到 HTTP/2 或 HTTP/3。如果暂时升不了,那就用域名分片——把资源分散到多个域名下,变相增加连接数。我优化过一个实时数据推送接口,从 HTTP/1.1 切到 HTTP/2 后,延迟降低了 40%。

1.3 接口版本管理策略——别让升级变成灾难

接口一旦上线,客户端可能有很多版本在跑。你改了接口,老客户端怎么办?这就是版本管理要解决的问题。

1.3.1 三种主流版本策略

策略 实现方式 优点 缺点
URL 路径版本 /v1/users, /v2/users 直观,容易路由 URL 变长,语义不纯
请求头版本 Accept: application/vnd.myapp.v1+json URL 干净,符合 REST 风格 调试不方便,需要额外解析
查询参数版本 /users?version=1 实现简单 容易被缓存污染,语义弱

我个人偏好请求头版本。为什么?因为 URL 路径版本虽然直观,但会让资源标识变得不纯粹。/v1/users/123 和 /v2/users/123 其实是两个不同的资源,但逻辑上它们代表同一个用户。请求头版本把版本信息放在元数据里,URL 只描述资源本身。

1.3.2 版本兼容性管理

版本升级时,最怕的是“改了一个字段,炸了一片客户端”。我总结了几条铁律:

  • 只加字段,不改字段:新增字段不影响老客户端。
  • 不删字段:老客户端可能还在用。真要删,等所有客户端升级完再说。
  • 不改字段类型:比如 id 从 int 改成 string,老客户端解析会崩。
  • 不改字段语义:status 之前表示“订单状态”,别改成“支付状态”。

我曾经踩过的坑:有一次我优化接口性能,把某个字段从实时计算改成缓存读取,结果字段值变了。前端没适配,页面展示的数据对不上。从那以后我定了个规矩:任何接口变更,哪怕只是性能优化,都要评估对客户端的影响。

1.3.3 版本生命周期管理

接口版本不能无限维护下去,否则代码里全是 if-else。我建议这样管理:

  1. v1 发布:初始版本。
  2. v2 发布:v1 进入“维护期”,只修 bug,不加功能。
  3. v3 发布:v1 进入“废弃期”,返回 Warning 头提示客户端升级。
  4. v4 发布:v1 下线,返回 410 Gone。

每个版本的维护周期,我建议至少留 6 个月。移动端 App 的升级率没那么快,你想想看,用户不升级你总不能远程删他 App 吧?

1.4 本章知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以把它当作接口设计的“检查清单”。

接口设计基础 · 知识体系 RESTful API 设计原则 HTTP 协议核心要点 接口版本管理策略 资源导向 URL · 标准 HTTP 方法 集合/单数层级 · 嵌套 ≤ 3 层 查询参数过滤/排序/分页 标准 HTTP 状态码优先 请求方法 · 幂等性区分 缓存控制 · Cache-Control / ETag 内容协商 · Accept / Content-Type 连接管理 · Keep-Alive / 队头阻塞 URL 路径 / 请求头 / 查询参数 兼容性:只加不改不删 生命周期:发布→维护→废弃→下线 维护周期 ≥ 6 个月 核心目标:可读 · 可缓存 · 可演进 · 可兼容

我的建议:刚开始做接口设计时,把这四个目标贴在工位上——可读、可缓存、可演进、可兼容。每次设计接口前扫一眼,能避免 80% 的坑。剩下的 20%,咱们在后面的章节里慢慢聊。