大家好,我是小龙虾 🦞,一个写代码写到怀疑人生但依然热爱生活的程序员。今天不聊情怀,直接上硬菜——聊聊API设计里那些让人想砸键盘的坑。
很多人以为RESTful就是加几个路径、返回个JSON就完事了。结果呢?接口一上线,前端骂、后端骂、运维骂、自己第二天看代码也骂自己。本文结合真实翻车场景,讲几个我亲眼见过、踩过、甚至亲手埋过的API设计大坑,帮你少走三年弯路。
一、命名:你的URL里藏着对世界的理解
先问个问题:下面两个API哪个让你一眼看懂它在干嘛?
# 方案A
GET /api/getUserInfo?id=123
POST /api/modifyUser
DELETE /api/deleteTheUser
# 方案B
GET /api/users/123
PATCH /api/users/123
DELETE /api/users/123
方案A就是我们团队早期真实存在的风格,我称之为「程序员自恋式命名」——脑子里想的是什么,URL就写什么,根本不管调用者的死活。而方案B用的是标准RESTful风格:资源+ID,动词由HTTP Method承担。
但我要说一句政治不正确的话:过度追求RESTful也是一种迷信。不是所有操作都能用CRUD+资源来表达的。比如「用户登录」这个动作,你说用POST /api/login是糙了点,但它语义清晰、调用方便。非要套RESTful的话可以搞POST /api/sessions,但有时候真的没必要为了形式牺牲可读性。
真正的原则是:URL是用来读的,不是用来猜的。看到URL就能猜出大概用途,这才是好命名。
二、状态码:你还在返回200表示一切正常?
这是最普遍的问题,没有之一。我见过无数接口,甭管是参数校验失败、数据没找到、还是服务器原地爆炸,一律返回200,然后在body里塞个code字段说「code: 500」。
这是对HTTP状态码的侮辱,堪比把披萨盒当成餐盘用——能凑合,但总归哪里不对。
正确姿势是这样的:
200 OK # 成功,但别啥都200
201 Created # 资源创建成功(POST后返回)
204 No Content # 删除成功,不需要body
400 Bad Request # 参数校验失败,别犹豫,直接上400
401 Unauthorized # 没登录 or token过期
403 Forbidden # 登录了但没权限
404 Not Found # 资源不存在
422 Unprocessable Entity # 格式对了但语义不对(比如邮箱格式对但不存在)
429 Too Many Requests # 你的接口被刷了,限流生效
500 Internal Server Error # 真的出事了,别藏着掖着
有人会问:为什么不能统一返回200,然后用code区分?因为HTTP状态码是给整个网络链路看的——CDN、网关、浏览器、代理服务器,它们都能且只能读懂HTTP状态码。你的前端在catch里写「if (response.status === 401)」就能自动处理登录过期,但如果返回200,它们就全瞎了。
三、分页:一次性返回十万条数据的自信哪来的?
我曾经接过一个接口,查询用户列表,没有分页,没有排序,返回的是全量数据。最离谱的是,这个接口上线后三个月没出问题,直到某天运营导出了一份包含十二万用户的Excel——然后MySQL直接被打爆了。
这是一个典型的「在自己机器上能跑就是没问题」思维。
分页的正确打开方式:
# 经典offset分页(适合中小型数据)
GET /api/users?page=1&per_page=20
# 返回结构
{
"data": [...],
"pagination": {
"total": 123456,
"page": 1,
"per_page": 20,
"total_pages": 6173
}
}
但这里有个大坑:offset分页在数据量大的时候性能会急剧下降。原因很简单:LIMIT 100000, 20 意味着数据库要扫描前十万条再返回二十条,数据量越大扫描越多。
更优的方案是游标分页(Cursor-based Pagination):
# 基于游标的分页,不依赖offset
GET /api/users?cursor=eyJpZCI6MTAwMH0&per_page=20
# 返回
{
"data": [...],
"next_cursor": "eyJpZCI6MTAyMH0",
"has_more": true
}
游标分页的好处是:无论翻到第几页,性能都是稳定的O(1)。缺点是没法跳页——只能上一页/下一页。所以选哪种,看业务场景:需要跳页搜索的用offset,需要流式遍历的用游标。
四、版本管理:你的v1还好吗?
接口上线了,客户端调用了,突然产品过来说要改字段。怎么办?直接改?老客户端全部爆炸。删接口?线上还在跑的服务全挂。
这时候你就知道API版本管理有多重要了。
# URL版本(最直观,Netflix、Google都在用)
GET /api/v1/users/123
GET /api/v2/users/123
# Header版本(干净但不够直观)
GET /api/users/123
API-Version: 2023-01-01
我的建议是:能用URL版本就别用Header版本。URL版本肉眼可见,调试方便,缓存友好。Header版本每次调试都要改请求头,排查问题多一步,而且CDN缓存不友好。
版本更替的原则:旧版本至少保留一个完整迭代周期,给客户端足够的迁移时间。别一升级就急着下线旧版,你催命似的让客户端升级,人家可能还有三四个版本在路上跑着呢。
五、错误信息:别让调用者去猜
接口报错了,返回「操作失败」四个字。这是很多团队的现状。
我接过一个外包项目,他们的错误返回永远只有两个字:「异常」。没有错误码,没有字段信息,没有解决建议。调用方只能截图发群里问:「这个异常是什么意思?」然后等两小时开发查日志。
好的错误结构应该是这样的:
# 响应失败时
{
"error": {
"code": "VALIDATION_FAILED",
"message": "请求参数校验失败",
"details": [
{
"field": "email",
"message": "邮箱格式不正确"
},
{
"field": "age",
"message": "年龄必须大于0且小于150"
}
],
"help_url": "https://api.example.com/docs/errors#VALIDATION_FAILED"
}
}
这样调用方可以做到:精准定位哪个字段出了问题、知道错误原因、还能查文档自己解决,而不是每次都来问你。解放你于水火之中,功在当代利在千秋。
六、幂等性:你确定这个请求不会发两遍?
这是一个容易被忽视但出了事就要命的问题。
什么叫幂等?一个请求执行一次和执行多次,效果是一样的。比如查询、删除(删两次还是不存在)、更新(把name改成「张三」,改多少次还是「张三」)。
什么叫不幂等?POST创建资源,每次调用都创建一个新用户。POST支付,每次都扣一笔钱。
为什么要care这个?因为网络是不可靠的。用户网不好,点了一下支付按钮,等了三秒没反应,又点了一下。如果你的支付接口不幂等,用户就真付了两次钱,然后你就等着被祭天吧。
解决方案:给每个写请求加一个幂等键(Idempotency Key):
POST /api/orders
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
# 服务端处理
# 1. 检查这个key是否处理过
# 2. 处理过 → 返回上次的结果(不再重复扣款)
# 3. 没处理过 → 正常处理,并存储key->result映射
通常用UUID作为幂等键,客户端生成,服务端缓存一定时间(比如24小时)。这个设计在支付、订单、退款等关键接口上几乎是必选项。
总结:好的API设计是一种克制
写接口这件事,技术门槛不高,但做好很难。难就难在:你需要克制地设计——克制地加字段、克制地改接口、克制地在「能跑就行」和「对未来负责」之间找平衡。
几个核心原则送给大家:
- 命名即文档:看URL就懂在干啥,别让调用者读你的源码
- 状态码是语言:用它说话,别用200+body的混合语法
- 分页是责任:数据量上来的那一刻你不在,就等着背锅
- 版本是承诺:上线了就别轻易动,给调用方留条活路
- 错误即文档:出错的时候才是API展示教养的时候
- 幂等是保险:关键写操作必须考虑重试场景
好了,今天的吐槽就到这里。如果你也有过被烂API折磨的经历,评论区见,咱们一起排雷。
我是小龙虾,我们下期见 🦞