我对接过无数第三方API,每次都觉得自己在上辈子造了孽。
你们有没有那种感觉:文档看了三遍,接口调不通,错误信息写的是操作失败,然后你去找技术支持,人家说"你查一下日志"。
我查你个大爷。
今天不吐不快,把这些年见过最离谱的API设计问题全抖出来,附带解决方案。收藏了以后面试都能用。
一、URL设计:你的动词放哪儿去了?
最常见的迷惑行为:一个接口长这样
POST /api/getUserInfo
POST /api/addNewProduct
POST /api/deleteOrderById
兄弟,HTTP方法你是一个都不认识吗?
GET是查,POST是增,PUT是改,DELETE是删,这是基本常识。但凡你学过一天RESTful都不会这么干。
正确的打开方式:
GET /users/123
POST /users
PUT /users/123
DELETE /users/123
名词复数,动词靠HTTP方法。这不是最佳实践,这是基本礼仪。
但问题来了——为什么这么多人还在用动词型URL?
因为很多后端框架的路由设计太直白了,/api/getUser写起来顺手,老板问起来也好解释。但这种设计本质上是把HTTP当传输管道用,完全浪费了协议本身的能力。
更深的问题是:当你用动词URL的时候,你的接口很难做统一的权限控制、缓存和日志。你没法对所有GET请求统一加缓存策略,因为有些"查询"藏在POST里。
所以我的建议是:先把HTTP方法用对,再谈别的。这是最基本的。
二、错误处理:你的400是认真的吗?
让我来猜猜你们公司接口的错误响应长什么样:
{
"code": 400,
"message": "操作失败",
"data": null
}
好,现在告诉我:这个400是什么意思?是参数校验失败?还是用户不存在?还是服务器数据库崩了?
你猜,你使劲猜。
很多接口的错误响应完全没信息量,message永远是"操作失败",code永远是那个数字。你根本不知道发生了什么,只能一遍遍试。
优秀的错误设计应该长这样:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "用户不存在,请检查user_id是否正确",
"details": {
"field": "user_id",
"provided_value": "abc123",
"reason": "格式不符,长度应为10位数字"
},
"request_id": "req_7f8a9b2c3d"
}
}
这个响应告诉你:问题是什么,在哪个字段,为什么,怎么追踪。
错误码要业务化,不要用技术状态码代替业务语义。USER_NOT_FOUND比404有用一百倍,因为前者直接告诉你"用户不存在",后者你还得猜404是哪个资源。
另外,HTTP状态码要合理使用:
- 400:客户端参数错误
- 401:未认证
- 403:已认证但无权限
- 404:资源不存在
- 422:语义正确但业务不允许
- 429:请求太频繁
- 500:服务端问题
不要所有错误都返回200然后在body里塞个success: false。这种设计让HTTP状态码完全失去意义,监控报警都做不好。
你说"我们返回200是方便前端统一处理"——那你方便了,nginx日志和监控报警工具找谁哭去?
三、响应结构:一会儿数组一会儿对象
这是另一个高频坑。同一个接口,有时候返回数组,有时候返回对象。
// 查一个
{
"id": 1,
"name": "产品A"
}
// 查多个
[
{"id": 1, "name": "产品A"},
{"id": 2, "name": "产品B"}
]
前端看到这个要骂人的。
统一响应结构是API设计的基本功。我的推荐方案:
// 查单个也包装成数组风格(推荐)
{
"data": {
"id": 1,
"name": "产品A"
},
"code": 0
}
// 列表用 pagination
{
"data": [
{"id": 1, "name": "产品A"},
{"id": 2, "name": "产品B"}
],
"pagination": {
"page": 1,
"page_size": 20,
"total": 100,
"total_pages": 5
},
"code": 0
}
所有响应都统一根级别结构:data放业务数据,code表示状态,message放错误信息。
这有什么好处?前端可以写一个统一的响应拦截器,所有接口的错误处理和loading状态都可以复用。你的代码量直接少一半。
四、Pagination:为什么你的分页这么难用
很多接口的分页设计是这样的:
GET /users?page=1&size=20
然后返回:
{
"users": [...],
"total": 1000
}
这个设计看起来没问题,但实际用起来要命:
第一,total字段在深层嵌套里,前端每次都要response.data.users这样访问,多层嵌套看多了头疼。
第二,没有下一页的标记。你不知道total_pages,只能自己算:Math.ceil(1000/20)=50,然后判断page>=50就说明到底了。服务端万一悄悄提高了limit,你算出来的边界全是错的。
第三,最致命的——没有游标(cursor)。当你数据在第二页和第三页之间被删除或新增时,页码式分页会出现数据错位。用户可能看到重复数据,或者漏掉一些记录。
更优的分页设计用cursor:
GET /users?cursor=eyJpZCI6MTAwfQ==&limit=20
返回:
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTIwfQ==",
"has_more": true
}
}
cursor是上一页最后一条记录的加密ID,下一页请求直接带这个cursor。这样不管数据怎么变化,分页结果都是稳定的。
当然,cursor分页不适合随机跳页场景。如果你的业务需要"第37页"这种精确访问,页码分页也可以接受,但一定要返回total_pages而不是让客户端自己算。
五、版本管理:你的v1什么时候是个头?
很多项目一开始就这样设计:
/api/v1/users
/api/v1/products
然后v2遥遥无期,v1跑了五年。
问题来了:你当初设计v1的时候,很多东西没想清楚,现在要改字段语义,怎么办?
几种常见策略:
URL版本(最常见):/api/v1/users → /api/v2/users
优点:直观。缺点:维护两套代码,v1还在被使用就不能删。
Header版本:Api-Version: 2024-01-01
优点:URL干净。缺点:调试不方便,CDN缓存也麻烦。
演进式:字段只增不改,旧字段标记deprecated但保留
优点:不用频繁开新版本。缺点:响应体会越来越臃肿。
我的经验:URL版本是最实用的。虽然不完美,但好维护、好调试、好让第三方知道他们在用什么版本。
更重要的是——不要在接口里暴露技术细节。数据库表名、内部字段名、能推导出其他接口的线索……这些都不应该出现在URL或响应里。API是对外的契约,不是内部实现的黑板报。
写在最后
API设计这件事,说到底是对调用者的尊重。
你的接口是给别人用的。人家在凌晨两点对着你的文档调接口的时候,如果错误信息写得像"网络异常请稍后再试"这种废话,人家心里一定在想:写这破接口的人是不是脑子有问题。
不想被骂,就好好做:错误信息要具体,分页要健壮,HTTP状态码要用对,文档要写清楚。不要觉得"反正能跑就行",你糊弄接口,接口就糊弄你——迟早的事。
以上。祝大家的API都能一次调通。