REST API设计:那些年我们踩过的坑,和想甩锅给HTTP协议的瞬间
做后端开发这么多年,我发现一个规律:所有程序员在职业生涯早期,都曾经真诚地认为HTTP状态码可以解决一切问题。直到他们遇见了产品经理说"这个接口返回成功就行",然后用户发现自己的钱消失了。
今天我们来聊聊REST API设计——不是那种"你好世界"级别的Hello World教程,而是实打实的、能在生产环境保你狗命的经验之谈。
一、状态码:别再只会200和500了
我知道你见过这种代码:
if (success) {
return 200;
} else {
return 500;
}
看到这种代码,我的感受是:又想提桶跑路了。
HTTP状态码是有限的,但人类想返回给客户端的信息是无限的。你以为400系列只是"客户端错了"?天真。来看看几个关键选手:
- 200 OK - 成功,但仅此而已。别往里面塞业务错误
- 201 Created - 资源创建成功,响应头带上Location
- 204 No Content - 成功但没内容,删除操作常用
- 400 Bad Request - 语义错误,请求参数有问题
- 401 Unauthorized - 没认证,先登录去
- 403 Forbidden - 认证了但没权限,死心吧
- 404 Not Found - 资源不存在,不是"用户不存在"而是"这个ID的资源不存在"
- 409 Conflict - 状态冲突,比如重复提交,这个用得最少但最该用
- 422 Unprocessable Entity - 格式对但语义错,validation失败用这个
- 429 Too Many Requests - 限流了,给客户端一个重试时间
- 500 Internal Server Error - 服务器炸了,但绝对不是你随手return 500的理由
我的忠告是:如果你的接口只会返回200和500,就像一个医生只会说"活着"和"死了"。
二、命名:URL是给人看的,不是给机器看的
看看这两种风格:
// 噩梦级
POST /api/v1/user/update
POST /api/v1/getUserInfo
GET /api/v1/queryOrderByUserIdAndDateRange
// 正常人类
PATCH /users/{userId}
GET /users/{userId}
GET /orders?userId=xxx&dateFrom=xxx
RESTful的核心是资源,不是动作。URL应该是一个名词,而不是一个动词。动作交给HTTP方法去表达。
几个黄金法则:
- 复数名词表示集合:
/users而不是/user - 嵌套表示从属关系:
/users/{userId}/orders - 查询参数用于过滤:
/orders?status=paid&limit=20 - 不要在URL里放动词,你的GET/POST/PATCH/PUT/DELETE就是动词
有人会说"这样嵌套太深了",但如果你的业务真的需要表达这种层级关系,那它就是合理的。别为了"扁平化"而强行扁平化,导致语义丢失。
三、版本管理:你的API需要退休计划
没有不变化的业务,只有还没到时候的改版。
常见的版本策略有三种:
// 1. URL路径版本(最直观,最常用)
GET /api/v1/users
GET /api/v2/users
// 2. Header版本(看起来干净,但其实很反人类)
GET /api/users
API-Version: 2024-01-01
// 3. Query参数(除非你有特殊执念,别用)
GET /api/users?version=2
我的建议是:URL路径版本。原因很简单——调试的时候你能直接看到在调哪个版本,CDN/网关配置也方便,还不会让调用方假装"忘了"升级。
版本升级的原则:
- 大版本不兼容,小版本保持兼容
- 旧版本要有明确的废弃时间表
- 给调用方足够长的迁移窗口(至少一个业务周期)
- 如果可能,提供自动迁移工具
四、错误响应:给调用方一个活下去的机会
一个好的错误响应,应该让调用方知道:发生了什么、哪里错了、怎么解决。
// 反面教材
{
"error": "Invalid request"
}
// 正面典型
{
"error": {
"code": "VALIDATION_FAILED",
"message": "请求参数验证失败",
"details": [
{
"field": "email",
"message": "邮箱格式不正确",
"rejectedValue": "this_is_not_email"
},
{
"field": "age",
"message": "年龄必须在18-150之间",
"rejectedValue": 16
}
],
"traceId": "abc123def456",
"documentation": "https://api.example.com/docs/errors/VALIDATION_FAILED"
}
}
关键点:
- code:业务错误码,客户端可以精准匹配处理逻辑
- message:人类可读的错误描述,给开发者看的
- details:具体哪个字段、什么值、什么错,在参数验证场景极其有用
- traceId:关联到日志系统的追踪ID,排查问题的钥匙
- documentation:让调用方知道去哪查文档,别发完错误就撒手不管
记住:你的API报错时,调用方可能是一个凌晨三点被叫醒的运维,也可能是一个刚睡醒的前端。别让他们在错误响应里玩解谜游戏。
五、分页:不做分页的API是不完整的API
如果你的GET接口返回全量数据,要么你的用户只有你一个人,要么你在准备删库跑路。
// Cursor-based分页(适合大表、实时数据)
GET /orders?cursor=eyJpZCI6MTIzfQ&limit=20
Response:
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6MTQzfQ",
"hasMore": true
}
}
// Offset分页(适合需要跳页的场景)
GET /orders?offset=100&limit=20
Response:
{
"data": [...],
"pagination": {
"total": 10000,
"offset": 100,
"limit": 20,
"hasMore": true
}
}
选择建议:
- 数据量大、实时性要求高 → Cursor分页,避免深分页的慢查询
- 需要显示"第X页,共Y条" → Offset分页,但请限制最大offset
- 无论哪种,都要在响应里告诉客户端"还有没有更多"
六、安全:基本修养,别等被hack了才后悔
几个老生常谈但总有人忘记的点:
- 不要在URL里传敏感信息(GET /users/12345?password=abc,GG)
- 速率限制(429状态码),防止刷接口
- CORS配置,别Access-Control-Allow-Origin: * 还觉得没啥
- 敏感操作要审计日志,谁在什么时间做了什么
- 别在错误信息里暴露系统细节,你的数据库表结构不该出现在错误消息里
结语
好的API设计就像好的代码注释——不是为了炫耀,而是为了让未来的自己(和同事)在凌晨三点出故障时,不用咬牙切齿地骂娘。
HTTP协议给了我们足够的表达能力,REST给了我们足够的设计哲学。剩下的,就是理解业务、尊重调用方、别偷懒。
如果你还在写return 200 or 500的接口,我的建议是:先别急着提桶跑路,给这个接口一次改过自新的机会。毕竟,代码可以重构,锅甩了就真的甩了。
祝你的API稳如老狗,凌晨三点永远不被叫醒。