你设计的API,我调用一次就想辞职:十年踩坑总结

2026-09-09 9 0

我调用过的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折磨了三天还没调通的愤怒前端。

为那个愤怒前端多花五分钟。

你未来的同事会感谢你。

相关文章

懒得折腾?AI工具代部署服务来了,让你省心省力省头发
为什么你的 API 总是不如别人家的?——从设计混乱到让人拍案叫绝的实战经验
写了5年代码才发现:API设计那些事儿,全是坑!
写了5年代码才发现:API设计那些事儿,全是坑!
我删了两千行ORM代码,换成原生SQL,然后产品经理给我买咖啡了
写SQL一时爽,线上火葬场——那些年我踩过的数据库性能坑

发布评论