我调用过的API,比我吃过的火锅还多。
好的API,用起来像德芙——纵享丝滑,调用方心情愉快,生活幸福。烂的API,用起来像在公共厕所吃火锅——每一步都是折磨,每一次返回错误码都想给设计者寄刀片。
今天不聊理论,只聊我踩过的坑、流过的泪、以及为什么某些API设计让我想穿越回去把设计师暴打一顿。
坑一:把HTTP状态码当装饰品
这是最普遍、最让人崩溃的问题。
很多后端工程师对状态码的态度是:200表示成功,500表示服务器挂了,其他?其他我也返回200,反正我把错误信息塞在response body里了。
举几个我亲眼见过的"艺术品":
// 登录失败,返回200,success字段是false
{
"success": false,
"message": "用户名或密码错误",
"data": null
}
// 删除成功,返回200,code字段标注错误类型
{
"code": "USER_NOT_FOUND",
"message": "用户不存在",
"data": null
}
// 服务器数据库宕了,也返回200
{
"code": "OK",
"message": "success",
"data": "Internal Server Error"
}
最后这个简直是心理恐怖片——success了,但data里写着Internal Server Error。
HTTP状态码是HTTP协议给调用者的礼物,是写在合同里的承诺。4xx表示你客户端的问题,5xx表示我服务端的问题。忽略它,就像签了合同然后把合同撕了——法律上这叫违约。
正确做法:用好状态码。201创建资源,204删除成功无body,400是客户端的错,401是未认证,403是已认证但没权限,404找不到,422参数校验失败,429请求过多,500服务器抽风。让调用者看一眼状态码就知道发生了什么,不用解析你的body。
坑二:命名随心所欲,想叫什么叫什么
很多API的URL命名,完全取决于工程师当天的心情。
GET /api/getUserInfo // get开头,lowercase,混搭
GET /api/get_user_info // get开头,snake_case
GET /api/user/info // 名词优先,但多了个info多余
GET /api/users/{id} // 复数名词,这才是正确姿势
GET /api/UserDetail // 大驼峰,你是Java吗
GET /api/user_detail/ // 尾部斜杠,有的有有的没有
POST /api/createOrder // post用create
POST /api/add_item // post用add,语义不一致
PATCH /api/modifyOrder/{id} // patch用modify
PUT /api/update_order/{id} // put用update
同一个项目里,四种风格并存。调用方每次对接都要先做一次考古发掘,理解你那天为什么选了这种命名风格。
正确做法(RESTful风格):
GET /users # 列表
GET /users/{id} # 单个
POST /users # 创建
PUT /users/{id} # 全量更新
PATCH /users/{id} # 部分更新
DELETE /users/{id} # 删除
名词复数形式,HTTP方法表达动作,不多一个字,不少一个字。简单,粗暴,有效。
坑三:分页是个玄学
说到分页,我见过的实现方式大概有十七种,每种都能让人血压飙升。
// 方式一:limit/offset(最常见,但最坑)
GET /users?limit=10&offset=20
// 方式二:page/size
GET /users?page=3&size=10
// 方式三:skip/take(这是哪门子方言)
GET /users?skip=20&take=10
// 方式四:start/num(你认真的吗)
GET /users?start=20&num=10
// 方式五:cursor(游标分页,性能好但实现复杂)
GET /users?cursor=eyJpZCI6MjB9&limit=10
更重要的是返回格式也五花八门:
// A项目:嵌套在data里
{
"data": [...],
"pagination": {
"total": 1000,
"page": 3,
"pageSize": 10
}
}
// B项目:平铺在外层
{
"items": [...],
"total": 1000,
"page": 3,
"pageSize": 10,
"hasNext": true
}
// C项目:完全没返回总数,调用方只能盲猜
{
"items": [...]
}
正确做法:统一分页参数(推荐page+page_size或者cursor),在响应里明确告诉调用方:总数、当前页、每页大小、是否有下一页。做一个有交代的API,别让调用方做无头苍蝇。
坑四:错误信息等于没说
错误信息是API的良心。但很多API的错误信息,让你感觉在和一台拒绝回答问题的Siri对话。
{
"error": "Bad Request",
"message": "Invalid parameter",
"code": 400
}
{
"error": "Error",
"message": "Something went wrong"
}
{
"code": "ERR_001",
"message": "Operation failed"
}
"Invalid parameter"——哪个参数?什么格式是对的?你倒是说啊!"Something went wrong"——谢谢你的哲学启蒙,但我现在更需要知道哪里出问题了。
正确做法:错误信息要包含足够上下文。
{
"error": "Validation Failed",
"message": "请求参数校验失败",
"code": 422,
"details": [
{
"field": "email",
"message": "邮箱格式不正确,应为 xxx@xxx.xxx"
},
{
"field": "age",
"message": "年龄必须大于0且小于150"
}
]
}
这样调用方可以直接渲染给用户,或者至少知道是哪里出了问题。
坑五:没有版本管理,想改就改
初期设计API的时候,所有人都说"先这样,后期再加版本管理"。后来你懂的,后期就是从来不。
没有版本管理的API,改动就是一场地震。昨天返回的name字段,今天变成了user_name。调用方什么都没改,但服务崩了。然后你接到的工单是:"API又坏了"。
正确做法:URL版本化。
GET /v1/users/{id} # 第一版
GET /v2/users/{id} # breaking changes时升级v2
# 同时维护一段时间,直到调用方都迁移完
版本号放在URL里,清晰、明确、不可忽视。调用方可以逐步迁移,不用被突然的breaking change追着跑。
坑六:把API文档当摆设
有些团队的API文档,大概是用来看的,不是用来用的。文档和实际API永远是两个世界。
文档写了需要token,但没写清楚是Bearer token还是自定义header。文档写了返回User对象,但没写清楚嵌套的Role对象里还有哪些字段。文档最后更新时间是两年前,但API已经重构了三轮。
正确做法:文档即代码,代码即文档。用OpenAPI/Swagger从代码注释里生成文档,让文档永远和实际保持同步。或者至少,在发布新API之前,先让一个完全没参与过开发的人按照文档走一遍流程。
总结:API是给调用方用的,不是给自己用的
很多后端工程师设计API的时候,想的是"我怎么实现最方便",而不是"调用方怎么用最爽"。这是API设计的最大误区。
记住三个原则:
一、一致性大于灵活性。命名、参数、响应格式,全项目统一风格。宁可无聊,不要混乱。
二、错误处理是API的脸面。好的错误信息比好的正常响应更重要。调用方八成的时间都在和错误打交道。
三、文档和API要同生共死。文档落后于实际,是技术债里利息最高的那笔。
下次设计API的时候,在脑子里开两个窗口:一个窗口是你自己,另一个窗口是一个被你的API折磨了三天还没调通的愤怒前端。
为那个愤怒前端多花五分钟。
你未来的同事会感谢你。