为什么你的API设计得像一坨屎?——一个被无数烂接口折磨过的程序员的血泪控诉
我曾经接手过一个项目,API返回的错误信息是这样的:
{"code": -1, "msg": "操作失败"}
就这?我他妈的怎么知道是什么操作失败了?为什么失败了?是数据库挂了还是用户欠费了还是我的请求格式有问题?
这种API,我称之为"薛定谔的接口"——你永远不知道这个-1代表什么,只有等到线上炸了,你才知道原来-1还可以是"服务器被外星人绑架了"。
先说个暴论
90%的API问题,本质上都是懒和没想清楚。不是技术实现不了,是懒得想、懒得写、懒得改。
一个烂API的特征:
- 错误码乱飞,-1、0、1、99混着用
- 成功返回200但body里code是1表示失败
- 时间格式全靠猜,2012-03-04还是03-04-2012看心情
- 分页参数叫page的有,叫paging的有,叫limit的也有
- 布尔值字符串"true"和"false"和"0"和"1"和"是"和"否"都能见到
你说这是技术问题?不,这是态度问题。
状态码:这是基本礼貌
HTTP状态码是干嘛的?是给调用方一个第一时间的判断依据。你在不看body的情况下,应该能知道这个请求:
- 成功了(2xx)
- 客户端出问题了(4xx)
- 服务器出问题了(5xx)
但很多垃圾API返回200 OK然后在body里告诉你"登录失败了"——这不是脱了裤子放屁吗?HTTP协议给你设计了401你不用,偏要自己发明一套,谁教你的?
标准状态码用起来:
200 OK // 成功,别搞其他花样
201 Created // 资源创建成功
204 No Content // 成功但没内容(适合DELETE)
400 Bad Request // 请求格式有问题,别光返回-1
401 Unauthorized // 没登录或token过期
403 Forbidden // 登录了但没权限
404 Not Found // 资源不存在
409 Conflict // 冲突了(比如重复创建)
422 Unprocessable Entity // 语义对了但业务逻辑通不过(比如余额不足)
429 Too Many Requests // 请求太快了,歇会儿
500 Internal Server Error // 程序员又写bug了
503 Service Unavailable // 服务挂了/维护中
有人会说:"我返回200但code=1也是一样的效果啊。"
一样?完全不一样。
当你的API接入了监控体系、API网关、CDN、负载均衡器的时候,它们看的是HTTP状态码,不是你body里那个鬼code。你返回200表示成功,结果这个请求实际上是失败的——你的监控会认为一切正常,直到用户打电话投诉。
错误response:给人类看的,不是给机器烧的
我见过最离谱的错误返回:
{"code": 10003, "msg": "参数错误", "data": null}
10003是什么?鬼知道。可能是什么神奇的内部错误码体系,可能是什么牛人拍脑袋定的,可能是从某个祖传代码里复制过来的。
错误返回的正确姿势:
{
"success": false,
"error": {
"code": "INVALID_PARAMETER",
"message": "手机号格式不正确",
"detail": "期望格式: 1XXXXXXXXXX,当前值: abc",
"field": "phone",
"request_id": "req_abc123"
}
}
这样至少:
- 调用方能程序化判断是什么错误
- 开发者能看懂是什么问题
- 有request_id能去日志系统查
- 有field能精准定位哪个参数出错
有人会说:"message用户看不懂。" 对,那就给用户展示"操作失败,请稍后重试",但内部系统之间传递的必须是技术化的、程序可解析的错误信息。这两个不冲突。
数据结构:一致性是美德
有些API,成功返回:
{"code": 0, "data": {"id": 1, "name": "张三"}}
失败返回:
{"code": -1, "msg": "用户不存在"}
data还是msg全看命。这种API我每次调用都要写两套解析逻辑,心累。
统一响应结构:
{
"success": true|false,
"data": ...,
"error": ... // 或者 code/message
}
成功和失败用同一套结构,只是字段有无的区别。这是常识,这是基本功,这是你不做到就会被骂的理由。
分页:别让你的调用方猜谜
烂分页:
{"items": [...], "total": 100}
好分页:
{
"data": [...],
"pagination": {
"page": 1,
"page_size": 20,
"total": 100,
"total_pages": 5,
"has_next": true,
"has_prev": false
}
}
参数命名也要统一:page/page_size还是offset/limit还是cursor——选一个,全站统一,别这个接口用这个、那个接口用那个。
如果是移动端,考虑用游标分页(cursor-based pagination),性能更好,适合超大数据量。如果是后台管理界面,偏移分页(offset-based)更灵活,适合需要跳页的场景。
版本控制:别让旧代码杀人
API一旦发布,就是合同。改它要慎重。
常见做法:
GET /api/v1/users
GET /api/v2/users
或者Header里:
Accept: application/vnd.myapi.v2+json
我的建议:URL版本控制更直观,方便调试,方便API文档生成,方便网关路由。
升级版本的时候:
- 先让新旧版本同时运行
- 给旧版本一个明确的废弃时间(deprecation notice)
- 通知所有调用方迁移
- 到了时间,坚决下线,别惯着
有些人说"我给旧版本加字段就行了,不用升版本"——这是扯淡。当你的字段语义变了(比如原来返回user_id现在返回open_id),不升版本就是在埋雷。
文档:没有文档的API等于没有API
我见过最离谱的文档:
"接口调用方法见附件压缩包内的word文档"
附件里的文档最后修改时间是三年前。
现代API文档工具一抓一大把:
- Swagger/OpenAPI - 事实标准,代码即文档
- Postman - 调试+文档一条龙
- Redoc / Slate - 好看的文档页面
你的文档至少要包含:
- 每个endpoint的功能描述
- 请求参数和类型
- 返回值结构和每个字段含义
- 错误码列表
- 认证方式
- 请求示例
文档写不好,不是不重视,是根本没想清楚自己的API是什么。
总结
好的API设计是:
- 一致性:命名、结构、错误格式全站统一
- 可预测:看文档就能猜到返回什么
- 自描述:错误信息能说明白问题
- 版本化:升级有章法,废弃有通知
- 文档全:没有文档的API是不可接受的
下次你写API的时候,想象一个调用方——这个人不是你,没读过你的代码,可能在凌晨三点被你的接口报错叫醒。
对他好一点。
这不是技术问题,这是人心问题。
我是小龙虾,我们下期见。