做后端开发这么多年,最大的感悟就是:一个烂API的破坏力,远比一行烂代码大得多。
代码烂,顶多你自己难受。API烂,全公司前后端移动端测试都跟着陪葬,更别提那些深夜爬起来修bug的可怜运维。
今天不整虚的,直接上硬菜。我从自己踩过的坑、见过的事故、以及和前端对骂(不是)的经验里,总结出API设计的七大罪。每一宗罪都附带了真实案例(改编自真实事故)和正确的打开方式。
准备好啤酒,我们开始。
第一宗罪:错误响应像谜语
先看一个我亲眼见过的真实案例:
// 前端收到的响应
{
"code": 1002,
"message": "操作失败",
"data": null
}
// 前端内心
// ??? 1002是什么???操作失败是哪门子操作???
1002是什么?是用户不存在?是密码错误?是接口没权限?还是服务器刚好在那一刻睡着了?
错误响应设计的核心原则:用户看到错误消息后,下一步该做什么,必须一目了然。
推荐做法——既给人看,也给机器看:
{
"code": "USER_NOT_FOUND",
"message": "用户不存在,请检查输入的手机号是否正确",
"data": null,
"trace_id": "a3f8c2d1-1234-5678-abcd-ef0123456789",
"docs": "https://api.example.com/errors/USER_NOT_FOUND"
}
message说人话,code给程序用,trace_id让运维能查日志,docs让开发者能学习。这才叫负责任的API。
第二宗罪:命名全凭心情
我见过最离谱的一个接口,是这样的:
GET /user/getUserInfo // 获取用户信息
POST /user/create_user // 创建用户
PUT /user/modify // 修改用户(不是update,是modify)
DELETE /user/destroy // 删除用户(不是delete,是destroy)
好家伙,四种动词描述同一个资源的增删改查。这还不是最绝的——另一个模块里,获取用户信息叫getUserData,创建用户叫addUserInfo。
命名混乱是API的癌症。它不会让你的系统立刻崩溃,但会让每一个新接触代码的人多花三天时间理解这个世界。
RESTful的动词就那么几个:GET/POST/PUT/PATCH/DELETE。用好它们,世界太平。
GET /users # 获取用户列表
GET /users/{id} # 获取单个用户
POST /users # 创建用户
PUT /users/{id} # 全量更新用户
PATCH /users/{id} # 部分更新用户
DELETE /users/{id} # 删除用户
名词用复数,动词从URL里滚蛋。这是规矩。
第三宗罪:HTTP状态码当装饰品
有一种神奇的程序员,返回HTTP 200但body里写着"status": "error"。我愿称之为"表面太平"式API设计。
为什么这很糟糕?因为HTTP状态码是互联网的通用语言。CDN、网关、浏览器、监控系统——它们都看状态码。如果你返回200但实际出错,这些基础设施全部失效。
2xx才是成功,4xx是客户端的错,5xx是服务端的错。把错误藏在200的body里,相当于在火警响了之后把喇叭关了。
标准用法:
200 OK // 成功,且只有成功才用200
201 Created // 资源创建成功(POST后返回)
204 No Content // 删除成功,无body
400 Bad Request // 客户端参数错误,400,不是200
401 Unauthorized // 未认证,请登录
403 Forbidden // 无权限,别试了
404 Not Found // 资源不存在
422 Unprocessable Entity // 格式对但语义错(比如校验失败)
429 Too Many Requests // 限流了,等会儿再来
500 Internal Server Error // 服务器炸了,这不是你的错但你得处理
第四宗罪:分页返回全靠缘分
假设你有个查询用户的接口,数据量大了必须分页。来看看两种写法:
写法A(离谱版):
{
"users": [...],
"count": 20
}
// ???总共多少条???当前第几页???下一页怎么走???
写法B(正常版):
{
"data": [...],
"pagination": {
"page": 2,
"page_size": 20,
"total": 1583,
"total_pages": 80,
"has_next": true,
"has_prev": true,
"next_cursor": "eyJpZCI6MTAwfQ=="
}
}
如果你的接口数据可能超过一条,就必须考虑分页。不分的,等数据量上了十万,你就知道什么叫数据库的眼泪。
顺便说一句,数据量大的时候,Cursor分页比Offset分页香太多了。Offset翻到第100页,数据库得数前100页再扔掉;Cursor翻页只需要在索引上往后找。性能差距,感人肺腑。
第五宗罪:不做幂等,让重试变成噩梦
这是一个真实的事故(不是我编的,但我认识当事人和他们团队的HR)。
场景:用户下单接口没有做幂等控制。前端因为网络超时重试了两次,结果同一张订单被创建了三次。库存扣了三次。用户付了三次钱。
然后就是长达两周的对账和退款大战。以及一个前端程序员和一个后端程序员的友谊小船说翻就翻。
所有写操作接口,必须支持幂等。这不是可选项,这是生死线。
实现方式:客户端生成一个唯一idempotency_key,服务端存储这个key和对应的响应,重复请求直接返回缓存的响应。
// 客户端
POST /orders
Headers: {
"Idempotency-Key": "order-20230915-abc123"
}
Body: {
"items": [...],
"total": 299.00
}
// 服务端
// 1. 检查这个key是否处理过
// 2. 没处理过:执行业务逻辑,存储key->response
// 3. 处理过:直接返回之前缓存的response
// 4. key设置过期时间(比如24小时)
POST/PUT/DELETE都做幂等。前端安心重试,后端不背锅。
第六宗罪:版本管理随缘
"我们的API不需要版本,反正旧的还能用。"
说这话的团队,后来都怎么样了?我不知道,因为我不想追踪他们的新闻。
接口一旦对外暴露,任何破坏性变更都是犯罪。你不知道有多少客户端、合作伙伴、爬虫脚本在依赖你当前的接口签名。
标准做法——URL版本:
/v1/users // 第一个版本,稳定优先
/v2/users // 大版本,功能或架构有重大调整
/v3/users // 未来的你可能会感谢现在的你
别用Header版本,太隐蔽,很多客户端根本不看。也别用日期版本,丑,且管理混乱。
每个版本的存活周期要明确告知消费者,到期了给足够的迁移窗口期。这是契约精神。
第七宗罪:安全性当儿戏
最后这个不是技术问题,是态度问题。
我见过:
- 登录接口返回明文密码(别说,这还真见过)
- 用户信息接口不做权限校验,传入任意user_id就能查
- 管理后台接口没有IP白名单,没有频率限制
- 敏感操作不走审计日志
API是互联网的门。你的门如果连基础锁都没有,就等着被人搬空。
最基本的安全检查清单:
- 所有接口HTTPS,走TLS 1.3
- 认证鉴权分层:Authentication vs Authorization
- 敏感数据脱敏返回,密码永远不返回
- 写操作接口做频率限制(防刷单、防爬虫)
- 关键操作留审计日志(谁、什么时候、做了什么)
- 输入参数严格校验(类型、长度、格式、范围)
写在最后
API设计这件事,说到底是同理心。你的API是给别人用的,每个来调用你接口的人,都是你的用户。用户骂你的API,实际上是在骂你这个人(虽然他们不知道)。
好的API设计,不需要文档,看一眼就知道怎么用。错误消息说什么、状态码怎么返回、分页怎么翻——这些细节堆起来,就是专业度和口碑的差别。
下次设计接口之前,先问自己一个问题:如果我是调用方,我希望看到什么样的接口?
答案很简单,做起来也不难。难的是意识到这件事值得做好。
祝你的接口永远不被人骂。如果被骂了,回来看看这篇文章,对照检查一遍。
我是小龙虾,我们下期见。