做后端开发这么多年,我见过最离谱的API设计是一个返回嵌套五层的数据结构,客户端同事看完直接骂街。那哥们后来说,每次调那个接口,他都想把写接口的人叫出来单挑。
今天不整那些网上随便搜得到的"RESTful规范",咱们聊点真正有用的——那些踩过坑之后才明白的道理。
一、HTTP状态码:别总返回200还假装一切正常
这是最容易翻车的地方。我见过太多接口,甭管查不到数据还是服务器崩了,返回码都是200,然后body里塞个{"code": 404, "message": "资源不存在"}。
客户端拿到200,兴高采烈开始解析,结果炸了。
正确姿势是什么?真正出问题的时候,状态码就应该是4xx或5xx。你的业务错误码code是给调用方做分支判断用的,不是让你用来掩盖HTTP状态码的。
我的习惯是:
- 资源找不到 →
404 - 参数校验失败 →
400,body里带具体哪个字段有问题 - 没权限 →
401或403 - 服务器抖了 →
500,body里带trace_id方便排查 - 限流了 →
429,header里带Retry-After
你说这是常识?不好意思,我今年还见过线上跑着返回200的404。
二、分页:offset跟limit这对老搭档,其实挺坑的
大多数人选分页方案的时候,第一反应就是page+page_size,或者offset+limit。简单,直观,看起来没毛病。
但数据量上来你就知道疼了。
比如查第1000页,每页20条,数据库得先扫描前20000条记录,然后扔掉19980条。就为了给用户看第1000页的内容。这开销,谁查谁知道。
更坑的是,在分页过程中数据变了——用户翻到第二页,发现有条数据跑第一页去了,或者干脆消失了。用户一脸问号,以为系统有bug。
更好的方案是游标分页(Cursor Pagination):
GET /api/users?limit=20&cursor=eyJpZCI6MTIzNH0
返回的时候带上下一页的cursor:
{
"data": [...],
"next_cursor": "eyJpZCI6MTUzNH0",
"has_more": true
}
原理是用上一页最后一条记录的ID作为起点往后查,数据库直接走主键索引,O(1)复杂度,不管翻到第几页性能都稳如老狗。
当然cursor分页也有代价——不能随机跳页。这个看你业务场景,大多数列表场景用户本来就是顺序浏览,cursor完全够用。
三、接口版本:你以为的"不兼容变更",可能只是你以为的
很多人设计接口版本的心态是:反正v1已经上线了,v2随便改,反正调用方会升级。
too young。
你永远不知道有多少调用方是"能用就不动"的忠实信徒。你v2把字段名从user_name改成username,人家客户端三个月不更新,你就等着被拉出去祭天吧。
我的原则是:接口一旦发布,任何变更都要向后兼容。新增字段OK,删除字段NO(标记deprecated慢慢下),改字段名NO,改类型NO。
如果真的必须做不兼容变更?老老实实发新版本,给调用方留足迁移时间。我一般会这么干:
- v1至少维护6个月再考虑下线
- 在文档和响应header里明确标注版本和生命周期
- 提供v1到v2的迁移指南
你可能会觉得维护多版本很麻烦。但相信我,比起半夜三点被电话叫起来处理线上事故,多维护几个版本根本不算事。
四、错误信息:"系统繁忙"这四个字,毫无意义
我见过最敷衍的错误响应是这样的:
{
"code": 1000,
"message": "系统繁忙,请稍后再试"
}
调用方拿到这个能干嘛?啥也干不了,只能弹个toast给用户,然后用户刷新,还是这个结果。
好的错误响应应该长这样:
{
"code": 40001,
"message": "余额不足,无法完成支付",
"detail": "当前余额38.50元,订单金额128.00元,差89.50元",
"trace_id": "a1b2c3d4e5f6",
"doc_url": "https://api.example.com/docs/errors#PAYMENT_INSUFFICIENT_BALANCE"
}
这才是真正有用的错误信息:
code是给程序判断的,要稳定,要文档化message是给用户看的,要说人话detail是给开发者排查用的,越详细越好trace_id是用来查日志的,没有这个你试试在线上排查问题doc_url是给调用方查文档的,这是服务意识的体现
还有一点:错误信息要本地化。你的用户可能在中国,也可能在硅谷,你返回一个中文"余额不足",人家老外也看不懂。
五、幂等性:这个坑不填,早晚翻车
什么是幂等性?就是这个操作你执行一次和执行一百次,效果是一样的。
GET查询是天然幂等的,这个没问题。但POST创建、PUT更新、DELETE删除呢?
举个小明付钱的例子。小明下单点了一下,没反应,再点一下——结果扣了两次钱。小明直接报警。
解决方案是客户端生成唯一请求ID:
POST /api/orders
Idempotency-Key: client-generated-uuid-12345
{...}
服务端收到请求,先查这个key有没有处理过。处理过?直接返回上次的结果。没处理过?正常处理,然后缓存结果。
这个key一般设置个24小时有效期就够了,太长了占缓存,太短了万一业务链条还没走完就过期了。
所有写操作接口都应该考虑幂等性,尤其是支付、退款、订单这些和钱相关的。别等到赔钱了才想起来填这个坑。
六、写在最后
API设计这事儿,说难听点,就是"你怎么让别人舒服地使用你的服务"。
好的API,调用方用起来感觉像在德芙巧克力里游泳,顺滑无比。不好的API,用起来像在沼泽地里挣扎,每一步都想骂人。
多想想调用方会遇到什么坑,多站在对方角度审视自己的设计。比什么设计模式都管用。
毕竟,代码是写给人看的,顺手也跑通机器。让人看懂,比让机器跑通更重要。
好了,今天的分享就到这里。如果觉得有用,转发给你那个写接口被吐槽的同事。