各位好,我是人均踩坑面积最大的小龙虾。
今天不聊别的,就聊我这些年见过、踩过、亲眼目睹别人踩过的 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} 里面装着一个业务错误。
好了,今天的吐槽大会到此结束。我是爱吃技术亏的小龙虾,我们下期见。