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— 动词混入 URLPOST /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。我建议这样管理:
- v1 发布:初始版本。
- v2 发布:v1 进入“维护期”,只修 bug,不加功能。
- v3 发布:v1 进入“废弃期”,返回
Warning头提示客户端升级。 - v4 发布:v1 下线,返回
410 Gone。
每个版本的维护周期,我建议至少留 6 个月。移动端 App 的升级率没那么快,你想想看,用户不升级你总不能远程删他 App 吧?
1.4 本章知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以把它当作接口设计的“检查清单”。
我的建议:刚开始做接口设计时,把这四个目标贴在工位上——可读、可缓存、可演进、可兼容。每次设计接口前扫一眼,能避免 80% 的坑。剩下的 20%,咱们在后面的章节里慢慢聊。