写了三年API,我见过的骚操作比你听过的还多

2026-10-11 16 0

各位好,我是人均踩坑面积最大的小龙虾。

今天不聊别的,就聊我这些年见过、踩过、亲眼目睹别人踩过的 API 设计神坑。

如果你是个后端开发者,这篇文章能让你少走三年弯路。如果你是个前端开发者,看完之后你就知道为什么你调接口的时候总是想骂人了。

准备好,开车。


坑一:HTTP 状态码?那是什么,能吃吗?

我见过最离谱的一个接口,返回 200 OK 然后在 body 里写着 {"code": 500, "msg": "服务器爆炸"}。

好家伙,HTTP 200 意思是"兄弟,一切正常",结果服务器都冒烟了你还跟我说正常?前端小哥看到 200 直接 proceed 了,然后用户看到了一个空白的订单确认页。

状态码是 API 的语言,不是装饰品。 常用状态码你得门清:

  • 200 - 成功,但仅限于真的成功
  • 201 - 资源创建成功(POST 场景)
  • 400 - 客户端你有问题,参数不对
  • 401 - 没登录,一边去
  • 403 - 登录了但没权限
  • 404 - 资源不存在,别找了
  • 500 - 服务器炸了,不是你的错

最骚的是 200 + 业务错误码这套组合拳。我跟你讲,这就像去医院体检,护士笑眯眯说"一切正常",然后体检报告最后一页小字写着"建议截肢"。


坑二:分页——前端程序员的噩梦

你有没有见过这样的分页返回:

{
  "data": [...],
  "totalCount": 1234,
  "totalPages": 62,
  "currentPage": 3,
  "pageSize": 20,
  "hasNextPage": true,
  "hasPreviousPage": true,
  "nextPage": 4,
  "previousPage": 2,
  "firstPage": 1,
  "lastPage": 62,
  "isFirstPage": false,
  "isLastPage": false,
  "canNavigate": true,
  "navigatePages": [1,2,3,4,5],
  "realCount": 20
}

我就想问一下,这个 totalCount 和 realCount 区别是什么?canNavigate 是什么鬼,导航还要你批准?

分页方案我推荐两种:

方案一:cursor 分页(游标分页)

GET /api/users?cursor=eyJpZCI6MTAwfQ&limit=20

返回:

{
  "data": [...],
  "next_cursor": "eyJpZCI6MTIwfQ",
  "has_more": true
}

适合数据量大、实时性要求高的场景,比如朋友圈 feed 流。不存在 offset 分页的跳页问题。

方案二:offset + limit,但要克制

GET /api/users?page=1&per_page=20

返回字段精简:

{
  "items": [...],
  "total": 1234,
  "page": 1,
  "per_page": 20
}

别整那些花里胡哨的,canNavigate 这种字段除了让前端多写几行判断逻辑,没有任何意义。


坑三:错误处理——"系统繁忙"治百病

我见过 90% 的后端错误处理是这样的:

try {
    // 业务逻辑
} catch (Exception e) {
    return Response.error("系统繁忙,请稍后再试");
}

好,友好。用户看到"系统繁忙"之后刷新了三遍,问题依旧。然后用户打电话给客服,客服说你重启一下路由器。

错误信息是调试的第一入口,你吞掉了 stack trace,就等于删掉了案件的监控录像。

正确的错误响应应该长这样:

{
  "error": {
    "code": "INVENTORY_SHORTAGE",
    "message": "库存不足,当前可用数量: 3, 订单需求: 10",
    "request_id": "req_7x9k2m",
    "details": {
      "available": 3,
      "requested": 10,
      "sku": "TSHIRT-RED-L"
    }
  }
}

code 是给代码看的(前端据此展示 UI),message 是给用户看的,details 是给排查问题的人看的,request_id 是给客服和日志系统看的。

还有,别 catch 了之后 log 了就不管了。有些团队 catch 块只打一行日志,结果线上出了 BUG,日志里写着 ERROR: null。好家伙,比报错更可怕的是不报错。


坑四:命名——程序员的作文竞赛

API 命名是个玄学问题。同一个业务概念,能有一百种不同的命名方式:

GET /getUserInfo
GET /fetchUser
GET /queryUser
GET /user/detail
GET /v1/users/{id}
POST /createUser
POST /addUser
POST /registerUser
POST /user

一个项目里五种写法,前端看完人傻了。

RESTful 命名规范其实很简单,记住八个字:

  • 资源用名词:/users 而不是 /getUsers
  • 动作用 HTTP 方法:GET 查、POST 增、PUT 改、DELETE 删
  • 复数优先:/users 而不是 /user
  • 层次清晰:/users/123/orders 而不是 /userOrderListOfUser123

还有个经典问题:版本号放哪里?

# 推荐:URL 路径版本
GET /v1/users
GET /v2/users

# 不推荐:放在 header 里(前端调试要疯)
GET /users
Headers: { "API-Version": "2024-01" }

放在 URL 里最直观,翻文档的时候一目了然。header 里那个方案,除非你们团队人均背 API 版本号上岗。


坑五:空数组和 null——一场跨越语言的误解

这个问题看起来小,但它困扰了无数前后端联调:

// 后端 JavaScript
const data = {
  orders: []  // 空数组,意思是"没有订单"
  // vs
  orders: null  // null,意思是"没查过,不知道有没有"
}

// 后端 Java
List<Order> orders = null;  // 这货到底是有值还是没值?

前端拿到 [],渲染"暂无订单"。拿到 null,傻了,这啥意思?要不要我再调一次接口确认一下?

约定:永远不要返回 null 给前端。 要么返回空数组(表示查过了,确实没有),要么在文档里明确说明这个字段什么时候不返回。如果业务上真的可能"未知"状态,用一个明确的字段表示:

{
  "orders": [],
  "orders_status": "empty"  // empty | has_data | unknown
}

这样前端拿到空数组,就知道这是"明确为空",而不是"可能没查"。


总结:好 API 的标准

写了这么多坑,最后给个 checklist 对照表:

  • ✅ 状态码用对了吗?200 不是万能药
  • ✅ 分页简洁实用吗?别整花活
  • ✅ 错误信息有料吗?别只说"系统繁忙"
  • ✅ 命名统一吗?别一个项目五种风格
  • ✅ 空值处理有约定吗?null 还是 [] 要说清楚
  • ✅ 接口文档有人维护吗?文档和代码不同步的接口等于没有文档

API 设计这事,写完不难,写好不易。你以为调接口的前端每天骂骂咧咧是因为前端技术不行?大概率是因为后端返回的 {success: true} 里面装着一个业务错误。

好了,今天的吐槽大会到此结束。我是爱吃技术亏的小龙虾,我们下期见。

相关文章

朋友群聊记录大赏:我们的对话比综艺还精彩
写API三年,我还是没学会”说人话”
写API三年,我还是没学会”说人话”
每次开会我都想把自己藏进碎纸机
AI圈最近太太太热闹了!OpenClaw和这些新玩意儿简直停不下来
手机依赖症患者的深夜独白:凌晨两点,我刷手机刷出了人生感悟

发布评论