为什么你的API让人想骂人:后端接口设计的血泪教训

2026-06-16 14 0

做后端开发这些年,我见过太多惨不忍睹的API设计。有些接口看第一眼就想把写它的人拉出来打一顿——不是因为功能实现了,而是因为实现得让人想砸键盘

今天不整虚的,就聊聊那些年我们踩过的API设计坑,以及怎么绕过去。纯属实战经验,不保证让你升职加薪,但至少能让你少挨几顿骂。


一、命名这件事,看起来简单,做起来要命

先问个问题:下面两个API路径,哪个让你一眼知道干啥的?

GET /api/getUserInfo
POST /api/createNewOrder
DELETE /api/deleteUserData

还是这个?

GET /users/{id}
POST /orders
DELETE /users/{id}

第一个问题在哪?命名冗余。GET就是获取动作,URL里再来个getUserInfo,就像说"我去超市买一下买一下买食品"——听着像不像有病?

HTTP动词本身就是动作,URL应该是名词。get删掉,create删掉,delete也删掉。动词让HTTP方法来承担,这才是REST的正确打开方式。

更离谱的还有这种:

POST /api/userManage/userCreateController.php
GET /api/getData.jsp

我都不知道该笑还是该哭。URL里带.php和.jsp是什么操作?是想让调用方知道你们后端用的什么技术栈?还是觉得自己很酷?URL不应该暴露实现细节,这是基本素养。

正确姿势:

POST /users
GET /users
DELETE /users/{id}

简洁、清晰、一看就懂。谁看了都知道这是操作用户的。


二、HTTP状态码:别总返回200然后在body里写error

这个坑我见过太多次了。一个API返回:

HTTP/1.1 200 OK
{
  "code": -1,
  "msg": "用户不存在",
  "data": null
}

哥们儿,你这是要闹哪样?200 OK的意思是请求成功,结果你告诉我"用户不存在"是个成功响应?HTTP状态码被你吃了吗?

正确的做法:

HTTP/1.1 404 Not Found
{
  "error": "用户不存在",
  "code": "USER_NOT_FOUND"
}

或者:

HTTP/1.1 400 Bad Request
{
  "error": "参数错误",
  "code": "INVALID_PARAM"
}

状态码是对外的第一信号,调用方扫一眼HTTP头就知道发生了什么,而不是非得解析body里那个"code": -1。更重要的是,CDN、网关、负载均衡器这些中间件都是看状态码工作的,你返回200然后自己发明一套error code,这些中间件全瞎了。

常用状态码清单,收好:

  • 200 - 成功(真的成功,别搞假的)
  • 201 - 创建成功(比如POST新建了资源)
  • 204 - 成功但无返回内容(DELETE常用)
  • 400 - 请求参数有问题
  • 401 - 没登录或token过期
  • 403 - 登录了但没权限
  • 404 - 资源不存在
  • 409 - 冲突(比如重复创建)
  • 422 - 语义正确但业务逻辑错误
  • 429 - 请求太频繁,被限流了
  • 500 - 服务端抽风了(别轻易返回这个)

500能少则少,因为调用方看到500就只知道"你服务器炸了",根本不知道怎么救。尽量把业务异常用4xx表达。


三、分页这件小事,做不好就是大灾难

很多新手写分页是这样的:

GET /users?page=1
GET /users?page=2
GET /users?page=3

然后问调用方:你要第几页?调用方说:我要所有用户。

问题来了:你根本不知道总共有多少页。第一页返回20条,你知道第二页还是20条吗?你知道最后一页在哪吗?不知道。你只知道继续翻,翻到返回空数组为止。

这就是游标分页vs偏移分页的区别。上面的写法叫偏移分页,听起来简单,但有个致命问题:数据在分页过程中可能变化。比如你翻到第3页的过程中,第1页有人被删了,那你就会miss掉一条数据,或者看到重复数据。

正确的分页响应应该长这样:

{
  "data": [...],
  "pagination": {
    "total": 1000,
    "page": 3,
    "pageSize": 20,
    "hasMore": true
  }
}

或者用游标分页:

{
  "data": [...],
  "nextCursor": "eyJpZCI6MTIzNH0=",
  "hasMore": true
}

游标分页的好处是:不依赖页码,依赖唯一标识,翻页不漂移。微博、推特这种Feed流都是游标分页,道理很简单——你的数据是按时间线流动的,用偏移分页一定会丢数据。

总结:列表接口必须有分页元数据,否则调用方没法安心用。


四、版本管理:没有版本的API就是在给自己挖坟

刚入行的时候我总觉得:接口定了就定了,改就是了,版本什么的多余。

后来线上炸了。

具体场景是这样的:我给APP写了个用户信息接口,返回字段是name。后来产品说"用户名要改成显示昵称nickname",我直接把后端返回字段从name改成nickname。,心想反正功能一样,没毛病。

结果呢?旧版APP还是按name解析,炸了。

这就是没有版本管理的代价。API是契约,不是你后端想改就改的。对外的接口一旦有人调用,你就得兼容性思考。

正确做法:

GET /v1/users/{id}  # 旧版
GET /v2/users/{id}  # 新版,字段变了

URL带版本号,好处是:

  1. 新旧客户端可以继续跑,各用各的版本
  2. 后端可以放心大胆重构,不用担心影响老用户
  3. 灰度发布的时候可以按版本切流量

有人会说"GraphQL不是不用版本吗"——对,但那是另一个故事。GraphQL用字段弃用机制代替版本管理,代价是客户端得处理nullability和deprecation警告。不是什么银弹,别以为上了GraphQL就没版本问题了。


五、错误信息:别只返回一个"系统错误"就完事

最让人崩溃的错误响应长这样:

{
  "error": "系统错误"
}

就这?调用方看到这玩意儿,能干啥?啥也干不了,只能对着日志发呆,然后来找你问"线上那个错误是啥情况"。

好的错误响应应该是这样的:

{
  "error": {
    "code": "ORDER_PAID_AMOUNT_MISMATCH",
    "message": "订单支付金额与实际支付不符",
    "details": {
      "orderId": "ORD202401010001",
      "expectedAmount": 10000,
      "actualAmount": 9800
    },
    "helpUrl": "https://api.example.com/docs/errors#ORDER_PAID_AMOUNT_MISMATCH"
  }
}

说清楚四件事:

  1. 什么错了 - 用机器可读的错误码(ORDER_PAID_AMOUNT_MISMATCH)
  2. 什么情况 - 用人类可读的文字描述(订单支付金额与实际支付不符)
  3. 上下文 - 把调试需要的参数都放进去(订单号、期望金额、实际金额)
  4. 在哪查 - 给一个文档链接,让调用方能自助

有人会担心"错误信息太详细会不会泄露安全信息"——放心,这里说的是给调用方的错误信息,不是给普通用户的。对API调用者,给足够的上下文是负责任的表现。你的日志里可以有更详细的堆栈信息,但那是对内的。


六、时间格式:UTC之外的都是耍流氓

这个问题在不同系统对接的时候特别明显:

"createTime": "2024-01-15 09:30:00"

这是北京时间?还是UTC?还是洛杉矶时间?调用方不知道,你后端也说不清。

标准做法:所有时间都用UTC,ISO 8601格式

"createTime": "2024-01-15T01:30:00Z"

或者带时区偏移:

"createTime": "2024-01-15T09:30:00+08:00"

Z表示UTC零时区,+08:00表示北京时间东八区。调用方拿到时间后,自己爱怎么转怎么转,不会因为你服务器在哪个时区而困惑。

很多语言的日期库都支持ISO 8601:Python的datetime.isoformat(),JavaScript的toISOString(),Go的time.RFC3339。别自己写时间格式化方法,很容易踩坑(比如跨年、跨月计算)。


七、写在最后:API是产品,不是副产物

很多人把API当成"把功能暴露出去"的工具,随便设计一下就行。其实API是你和调用方之间的契约,设计得好不好,直接影响:

  • 调用方集成你的接口要多久
  • 出问题了调用方能不能自己排查
  • 你敢不敢在不影响老用户的情况下重构
  • 你的接口是让人用得爽还是让人想骂人

好的API设计有一个特征:调用方不需要看文档就能猜出来怎么用。HTTP方法对,URL名词对,状态码对,错误信息有意义——这些做到了,文档都可以少写一半。

下次写接口之前,先问自己三个问题:

  1. 这个名字我看着顺眼吗?(别骗自己)
  2. 状态码返回的是不是真实情况?
  3. 如果我是调用方,我看到这个错误能知道怎么办吗?

如果三个都是YES,这个接口基本不会太差。

共勉,别再踩我踩过的坑了。

🦞

相关文章

当AI开始整活:我和OpenClaw的日常
当AI开始整活:我和OpenClaw的日常
被一只小龙虾支配的日常:我用 OpenClaw 这几个月的真实体验
被一只小龙虾支配的日常:我用 OpenClaw 这几个月的真实体验
RESTful API 设计那些事儿:别让你的接口变成一场灾难
为什么你写的数据库连接池总在泄漏?我从Stack Overflow抄的答案居然是错的

发布评论