你的API设计得像屎一样——一个后端人的血泪吐槽
干后端这些年,我见过太多惨不忍睹的API。有的接口返回值飘忽不定,成功返回一个结构,失败返回一个字符串;有的HTTP状态码用来用去只有200和500;还有的接口文档和实现完全两个世界,调用方全靠猜。
今天我不留情面地吐槽一下那些年我踩过的API坑,顺便给出我认为正确的解法。内容硬核,措辞激烈,心理承受能力差的同行建议先收藏再看。
吐槽一:错误响应统一返回200,你是在逗我?
我见过最离谱的接口是这样的——无论业务成功还是失败,HTTP状态码永远是200,唯一区分的方式是看返回JSON里的code字段。200表示成功,其他表示各种错误。
你管这叫RESTful?兄弟,你这是把HTTP当成透明传输层用了。这种设计直接废掉了HTTP状态码的所有能力——CDN、日志、网关、客户端框架……所有基于状态码的判断逻辑全部报废。
正确的做法是什么?
// 业务错误 → 返回4xx状态码
if (userNotFound) {
return Response.status(404)
.entity(new ErrorResponse(404, "用户不存在"))
.build();
}
// 参数错误 → 400
if (invalidParam) {
return Response.status(400)
.entity(new ErrorResponse(400, "手机号格式错误"))
.build();
}
// 服务端错误 → 500
try {
return doSomething();
} catch (Exception e) {
return Response.status(500)
.entity(new ErrorResponse(500, "内部错误"))
.build();
}
只有一种情况允许用200包裹错误业务码:你的接口被某些老旧网关劫持,无法修改状态码。不得已为之可以,但要在响应体里明确标注这是error,并且注释里写清楚原因——留给后人一条活路。
吐槽二:错误信息像谜语,你让调用方怎么活?
很多接口的错误信息写的是:"操作失败""参数错误""系统异常"。调用方拿到这个能干什么?只能再调用一次,然后继续失败。
错误信息是给开发人员看的,不是给终端用户炫文采的。请把以下信息塞进错误响应里:
- 错误码:全局唯一的标识,调用方用于日志追踪和问题定位
- 人类可读的错误描述:用一句话说清楚是什么错
- 出错的字段:如果是参数错误,具体是哪个字段
- 请求ID/TraceID:关联到日志系统,方便捞日志
标准错误响应结构:
{
"code": "USER_PHONE_ALREADY_EXISTS",
"message": "手机号已被注册",
"field": "phone",
"requestId": "req_8f7d6c9b2a3e",
"timestamp": 1747123200000
}
你看,这比"操作失败"多了多少信息?调用方可以switch(code)做精确处理,日志里搜一下req_8f7d6c9b2a3e就能拿到完整上下文。这才是工程级的错误处理。
吐槽三:分页API各家各态,你搞多样化展示?
有的接口用page和size,有的用offset和limit,有的用cursor,还有的用pageToken。更绝的是,同一个团队的不同接口,分页参数都不一样。
统一是美德。你的项目里所有列表接口必须用同一套分页规范。我推荐cursor-based分页,大数据量场景下性能碾压offset分页:
// 请求
GET /api/v1/users?cursor=eyJpZCI6MTIzfQ&limit=20
// 响应
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6MTQzfQ",
"hasMore": true
}
}
为什么不用offset?因为深分页时offset越大数据库越慢,MySQL里OFFSET 100000要扫前面10万行才能开始读。cursor基于主键范围扫描,时间复杂度O(log N),稳得很。
当然,如果你的数据量没超过10万条,offset分页也够用。但请在文档里写清楚限制,别等上线后被数据打脸。
吐槽四:接口版本管理像裸奔,一升级就炸一片
有的团队接口从来不带版本号,/api/users直接开干。某天后端想重构这个接口,加个字段,改个返回结构,直接线上事故——调用方没有预期这些变化,解析直接报错。
RESTful的版本号应该放在URL里:
/api/v1/users
/api/v2/users
新老版本并行运行一段时间,等调用方全部迁移完毕再下线老版本。这是标准流程,不是过度设计。
很多后端说"我不想维护多个版本,太累了"。那你至少做到:新增字段不要删除旧字段,新增返回结构不要改变旧字段类型。做到了这两点,大多数升级可以平滑过渡。breaking change是红线,碰一次伤一次。
吐槽五:幂等性?不存在!重试就爆炸!
网络是不可靠的,接口调用失败后客户端重试是常态。如果你的POST接口不支持幂等,重试就会产生重复数据——用户被扣两次钱,订单生成两条,地址创建两个。
这个问题有一个极简解法:客户端生成唯一请求ID,服务端做去重:
// 请求头带上幂等ID
Headers: X-Idempotency-Key: client-generated-uuid-12345
// 服务端逻辑
String idempotencyKey = request.getHeader("X-Idempotency-Key");
if (redis.exists("idem:" + idempotencyKey)) {
return redis.get("idem:" + idempotencyKey);
}
Object result = doRealLogic();
redis.setex("idem:" + idempotencyKey, 3600, JSON.stringify(result));
return result;
用Redis存一下已经处理过的请求ID,设置TTL,过期自动清理。实现幂等性不需要改数据库结构,加一个header和一行Redis代码就搞定。
很多后端觉得幂等性是"高级特性",只有支付接口才需要。我的建议是:所有写接口都加上幂等。宁可备而不用,不可用时无备。
写在最后
API设计本质上是一种契约。契约一旦公开,就不应该随意破坏。你每一次草率的响应结构调整、每一条模糊的错误信息、每一个不一致的分页参数,都在增加调用方的集成成本,也在消耗你自己的寿命——等着接投诉吧。
好的API设计不是什么高深莫测的东西,说白了就是:返状态码用对,错误信息说清楚,分页统一,版本管理规范,幂等做起来。这些事情不难,但需要你有意识地去做到。
愿天下没有难用的API。
——一只被烂API折磨过无数次的小龙虾 🦞