为什么你的REST API让人想砸键盘?一位后端开发者的血泪吐槽
干过后端开发的基本上都踩过这种坑:对接一个第三方API,文档写得跟天书似的,返回数据结构嵌套八层,关键字段还是个动态key,文档三年没更新,接口报500连个错误信息都没有。打电话给技术支持,对方说"这个接口一直是这样子的呢"。
我干这行这么多年,见过太多烂API了。今天不吐不快,顺便聊聊什么叫真正好用的API设计。
REST不是什么了不起的东西,别把它当圣经
很多人一聊API设计就"RESTful",仿佛不加个ful就不够专业。但现实是,HTTP协议那几条动词(GET/POST/PUT/DELETE)根本装不下业务逻辑的复杂度。
举几个常见的坑:
- 一个搜索接口,GET请求带几十个过滤参数,URL长得能绕地球三圈,Nginx直接报414
- "更新"语义用POST,"创建"也用POST,"部分更新"还是用POST,HTTP动词?不存在的
- 一个业务操作涉及三个资源的原子更新,REST设计下你要调三次API,中间任何一次失败都不知道怎么回滚
我的观点:REST是风格,不是教条。你的业务逻辑决定你的API长什么样,而不是REST规范决定你的业务逻辑该怎么做。
好API的第一个标准:连傻子都能看懂
API是给人类用的,不是给机器用的。机器只管执行,人类得理解、记忆、调试、排查。
什么叫好的URL设计:
# 差的:嵌套地狱
GET /api/v1/users/12345/orders/6789/items/111
# 好的:扁平化,可读性强
GET /api/v1/orders/6789/items
GET /api/v1/users/12345/orders
什么叫好的命名:
# 差的:缩写狂魔,看不懂
GET /api/usr/ord/lst?tp=1&st=pend
# 好的:完整单词,语义清晰
GET /api/users?status=pending
GET /api/orders?type=shipping
URL里的名词是复数还是单数其实不重要,重要的是整站统一。最烦的就是一套接口里有的用单数有的用复数,也不知道谁先谁后,反正就是乱。
错误处理:别让调用方猜谜
这是最容易暴露后端开发者情商的地方。想象一下这个场景:
你调一个接口,返回{"code": 500, "message": "Internal Server Error"}
你打电话给厂家,厂家说"这个是网络波动导致的呢"。你问"那我重试有用吗",对方说"可能有用呢"。
这种错误处理等于没做。
好的错误响应应该长这样:
{
"code": 10401,
"message": "用户积分不足,无法兑换此商品",
"detail": {
"userPoints": 50,
"requiredPoints": 100,
"shortage": 50
},
"action": "充值积分或选择其他商品",
"traceId": "abc123def456"
}
错误码要分类清晰:10xxx是用户侧错误(参数错误、权限不足),20xxx是业务逻辑错误(余额不足、配额超限),50xxx是服务侧错误(数据库挂了、依赖服务超时)。调用方一看码就知道该自己排查还是该打电话骂你们。
还有,traceId必须要有,出了生产问题调用方报过来你能直接搜日志,没有traceId的错误响应就是在耍流氓。
版本管理:向前兼容是基本礼貌
接口要版本号,这是常识。但很多人做版本号就是挂个v1/v2,然后"这次我们改了大返回结构",原有调用方全部炸裂。
真正的版本管理原则是:新增字段可以,删除字段不行,改字段名不行,改字段类型不行。如果必须要改,请走新的版本号,并且给旧版本至少三个月的过渡期。
# URL版本是最直观的方式
GET /api/v1/users/123
GET /api/v2/users/123
# Header版本适合复杂场景
GET /api/users/123
Accept: application/vnd.myapi.v2+json
我自己倾向URL版本,因为调试的时候一眼就能看出在测哪个版本,不用去翻Header。
性能优化:别把数据库裸奔暴露给前端
N+1查询问题,懂的都懂,不懂的迟早会懂。
# 差的:一次列表请求,N次详情查询
GET /api/v1/orders
# 返回
[
{"id": 1, "userId": 101, "productName": null},
{"id": 2, "userId": 102, "productName": null}
]
# 然后前端逐个调
GET /api/v1/users/101
GET /api/v1/users/102
# 页面加载:1 + N 次请求,数据库:N次额外查询
# 好的:一次请求,返回完整数据
GET /api/v1/orders?include=user,items
# 返回
[
{
"id": 1,
"user": {"id": 101, "name": "张三"},
"items": [{"id": 1, "name": "小龙虾"}]
}
]
include参数或者fields参数是标配,让调用方决定要拿什么数据。既减少网络传输量,又减轻数据库压力。不做这个的前端API,要么是早期草台班子,要么是后端懒得写。
幂等性:重试不炸是基本素养
网络不稳定的时候,前端重试是常态。如果你的接口不具备幂等性,重试一次就多扣一次钱、多创建一条订单、多发一封邮件,那这个API就是定时炸弹。
哪些操作天然幂等:
- GET:只读操作,天然幂等
- PUT:用固定数据覆盖,幂等
- DELETE:删除一次和删除一百次结果一样,幂等
哪些操作需要特殊处理:
- POST创建订单:不幂等,需要通过业务ID或唯一索引防止重复
- 扣款/扣积分:不幂等,需要幂等键(idempotency key)
# 幂等键实现示例
POST /api/v1/orders
Idempotency-Key: client-generated-uuid-12345
# 服务端根据key缓存响应,重复请求直接返回缓存结果
所有涉及金钱的操作,幂等键是必选项,不是可选项。
文档:没有文档的API等于没有API
最后这条可能最重要,也可能最容易被忽视。文档不是写给自己看的,是写给调用方看的。
好文档三要素:
- 每个字段都要有说明:类型、含义、可能的值、是否必填
- 要有完整示例:请求示例和响应示例,能直接复制粘贴调试的那种
- 要有错误码对照表:每个错误码代表什么,调用方该怎么处理
我见过最离谱的文档是一张截图,上面写着"详见接口文档",然后接口文档就是个Word文档在另一个服务器上,链接已经404了。
工具推荐:Swagger/OpenAPI是标配,写代码的时候顺手就生成了,别偷懒。手写文档最大的问题不是初始工作量大,是后续维护不起来,最后文档和接口变成两个世界。
写在最后
API设计这事,说难听点,就是"你行你上"和"我上我也行"之间的差距。看起来谁都能设计,但设计出来好不好用、稳不稳定、调用方骂不骂你,那就是另一回事了。
核心原则就三条:
- 调用方是傻子:你的API要让傻子也能调通
- 错误信息是信任:错误处理做得好,调用方才愿意重试而不是直接换供应商
- 文档是合同:文档写了什么,接口就得是什么,改了接口必须同步改文档
做到这三条,不敢说你的API能封神,但至少不会让人想砸键盘。
至于那些文档三年不更新、错误码永远是{"code": 0, "msg": "success"},连个traceId都没有的API——
建议转行送外卖,真的。