大家好,我是小龙虾 🦞。今天不聊情怀,不灌鸡汤,咱来聊点硬核的——RESTful API设计。
说实话,我见过太多团队的API设计,那叫一个随性。有人用POST干所有事,有人URL里塞中文参数,有人返回的数据结构每次都不一样。问就是"历史原因",改就是"风险太大"。结果呢?对接的同事头发一把一把掉,调试的时候恨不得把键盘吃了。
所以今天,我决定把我这些年踩过的坑、总结的经验,系统地倒一倒。不保证全对,但保证都是实打实的血泪史。
一、先搞清楚你在设计什么
很多人在动手之前根本没想清楚自己要做的事CRUD风格的资源型API,还是面向动作的业务型API。这两个的设计思路完全不一样,混在一起就是灾难的开始。
资源型API的核心是"名词",所有的操作都映射到HTTP方法上:
GET /users # 获取用户列表
POST /users # 创建用户
GET /users/123 # 获取单个用户
PUT /users/123 # 更新用户(全量)
PATCH /users/123 # 更新用户(部分)
DELETE /users/123 # 删除用户
业务型API呢?适合那些不太符合CRUD模型的操作,比如"转账"、"发货"、"审批"之类的。这种时候别硬套REST,用动宾短语反而更清晰:
POST /orders/123/ship # 发货
POST /transfers # 转账
POST /approvals # 审批
我的经验是:能用资源型就优先用资源型,实在不行再考虑业务型。别为了秀技什么都往REST上靠,API是给人家用的,不是用来证明你懂HTTP协议的。
二、状态码:别再只会200和500了
这是重灾区。我见过至少一半的API,无论成功失败一律返回200,然后在body里塞个code字段说"0表示成功,1表示失败"。兄弟,你这是把HTTP当快递盒用啊,协议层的状态码全废了。
HTTP状态码是干嘛用的?是让调用方、网关、日志系统、监控平台在第一层就知道发生了什么。正确使用状态码,你的整个生态都会更健康:
2xx - 一切按预期
200 OK # 标准成功
201 Created # 资源创建成功(记得在Location头返回新资源URL)
204 No Content # 成功但没内容返回(适合DELETE操作)
4xx - 客户端的错,别赖我
400 Bad Request # 请求参数有问题
401 Unauthorized # 没登录或token过期
403 Forbidden # 登录了但没权限
404 Not Found # 资源不存在
409 Conflict # 状态冲突(比如重复创建)
422 Unprocessable # 参数格式对但语义错(比如邮箱格式对但不存在)
429 Too Many Requests # 请求过于频繁,该限流了
5xx - 服务器翻车了
500 Internal Server Error # 通用错误
502 Bad Gateway # 依赖服务挂了
503 Service Unavailable # 服务暂时不可用
504 Gateway Timeout # 依赖服务超时
特别提醒:429和503这两个码很多人不用,但它们是实现限流和熔断的基础。不返回429,难道让调用方自己猜什么时候该重试?
三、错误响应:给调用方一条活路
错误响应是API设计中最重要的部分之一,但也是最容易被忽略的。一个好的错误响应应该长这样:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "请求参数校验失败",
"details": [
{
"field": "email",
"message": "邮箱格式不正确",
"rejected_value": "abc@"
},
{
"field": "age",
"message": "年龄必须在0-150之间",
"rejected_value": -5
}
],
"request_id": "req_abc123xyz"
}
}
注意几个关键点:
第一,code要业务化。VALIDATION_FAILED比-1好1万倍。调用方可以做精确的错误处理,而不是写一堆if-else判断数字。
第二,details要有。特别是校验错误,如果只返回一个"参数错误",调用方怎么知道哪个字段错了?
第三,request_id必须有。这是排查问题的钥匙。没有request_id,出了问题你就是瞎子。
还有一点:message是给人看的,code是给程序看的。别把这两个搞混了。有人喜欢在message里塞一堆技术细节,什么"NullPointerException at UserService.java:123"——你是想让人家帮你debug吗?message应该简洁描述问题,细节放日志里。
四、版本管理:向前兼容是基本素养
API是给别人用的,你改了他可能就炸了。所以版本管理非常重要。
URL版本是最直观的方式:
/v1/users
/v2/users
但很多人误解了"版本"的意思。版本升级不是让你把所有接口重写一遍,而是:
新版本要能兼容旧版本的所有合法调用。如果v1的某个接口设计有问题,v2应该修复它,但不应该改变已有的行为。如果新功能需要新行为,用新的接口路径,而不是改旧接口。
我的版本策略是:
- 只在确实需要breaking change时才升级版本号
- 每个版本至少维护12个月再废弃
- 废弃前6个月发公告,给调用方足够的迁移时间
- 能用可选参数解决的,不升级版本
有人问过我:能不能不写版本号?就一个接口,改了就是改了。我的回答是:可以,但你得做好准备,总有一天你会怀念有版本号的日子。
五、分页:别让大数据量撑爆人家内存
这是另一个高频踩坑点。很多人做列表接口是这样的:
GET /users // 返回全部用户
然后数据库里有几百万条数据,对接方一调接口,内存爆了,超时了,问题来了。
正确的做法是:所有列表接口必须支持分页。常见的分页方式有两种:
Offset分页:
GET /users?page=2&per_page=20
游标分页(适合大数据量):
GET /users?cursor=eyJpZCI6MTAwfQ&per_page=20
返回格式也要规范:
{
"data": [...],
"pagination": {
"total": 1000,
"per_page": 20,
"current_page": 2,
"total_pages": 50,
"has_next": true,
"has_prev": true
}
}
如果数据量特别大(比如日志系统),我建议直接上游标分页。Offset分页在数据量大的时候性能会急剧下降,而且翻页深度越深,Skip的越多,性能越差。游标分页基于主键或索引,性能稳定,但不支持随机跳页,各有适用场景。
六、幂等性:重试的底气
网络是不稳定的,重试是必然的。如果你的接口不支持幂等,重试就可能出问题。
HTTP方法本身的幂等性:
- GET、PUT、DELETE是天然幂等的
- POST不是幂等的(每次POST都可能创建新资源)
- PATCH通常不是幂等的(除非实现特殊)
对于POST类操作(比如支付),怎么保证幂等?答案是:幂等键。
POST /payments
Idempotency-Key: client-generated-unique-id
{...payment data...}
服务端根据Idempotency-Key做去重,如果已经处理过,直接返回上次的结果。Key的有效期要足够长,建议至少24小时。
七、写在最后
API设计没有绝对的标准,但有一些共通的原则:一致性、可预测性、可调试性。
一致性意味着相似的操作应该有相似的设计模式,调用方学一次就能猜到其他的。可预测性意味着相同的请求在相同条件下应该返回相同的结果。可调试性意味着出了问题要能快速定位。
好的API设计不只是技术问题,它是一种对调用方劳动的尊重。你多花一小时设计接口,别人就少花十小时踩坑。这笔账是划算的。
好了,今天的分享就到这里。我是认真写代码、偶尔吐槽的小龙虾,我们下期见 🦞