为什么你的API总被骂?聊聊那些让人又爱又恨的接口设计

2026-09-20 7 0

做后端开发这么多年,我见过最离谱的API设计是一个返回嵌套五层的数据结构,客户端同事看完直接骂街。那哥们后来说,每次调那个接口,他都想把写接口的人叫出来单挑。

今天不整那些网上随便搜得到的"RESTful规范",咱们聊点真正有用的——那些踩过坑之后才明白的道理。

一、HTTP状态码:别总返回200还假装一切正常

这是最容易翻车的地方。我见过太多接口,甭管查不到数据还是服务器崩了,返回码都是200,然后body里塞个{"code": 404, "message": "资源不存在"}

客户端拿到200,兴高采烈开始解析,结果炸了。

正确姿势是什么?真正出问题的时候,状态码就应该是4xx或5xx。你的业务错误码code是给调用方做分支判断用的,不是让你用来掩盖HTTP状态码的。

我的习惯是:

  • 资源找不到 → 404
  • 参数校验失败 → 400,body里带具体哪个字段有问题
  • 没权限 → 401403
  • 服务器抖了 → 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,用起来像在沼泽地里挣扎,每一步都想骂人。

多想想调用方会遇到什么坑,多站在对方角度审视自己的设计。比什么设计模式都管用。

毕竟,代码是写给人看的,顺手也跑通机器。让人看懂,比让机器跑通更重要。

好了,今天的分享就到这里。如果觉得有用,转发给你那个写接口被吐槽的同事。

相关文章

你的「可扩展设计」正在悄悄谋杀代码的可读性
分布式事务:2PC太重、Synchronized太土,Saga才是微服务的体面退出方式
【神器推荐】还在为部署AI工具秃头?一键部署服务来了,拯救你的头发!🦞
写API这事儿,10个人里有9个没想明白
写API这事儿,10个人里有9个没想明白
AI Agent到底能不能替你做主?我找了3个场景实测,结果有点意外

发布评论