API设计里那些让人想砸键盘的骚操作

2026-09-14 5 0

做后端开发这么多年,最大的感悟就是:一个烂API的破坏力,远比一行烂代码大得多。

代码烂,顶多你自己难受。API烂,全公司前后端移动端测试都跟着陪葬,更别提那些深夜爬起来修bug的可怜运维。

今天不整虚的,直接上硬菜。我从自己踩过的坑、见过的事故、以及和前端对骂(不是)的经验里,总结出API设计的七大罪。每一宗罪都附带了真实案例(改编自真实事故)和正确的打开方式。

准备好啤酒,我们开始。


第一宗罪:错误响应像谜语

先看一个我亲眼见过的真实案例:

// 前端收到的响应
{
  "code": 1002,
  "message": "操作失败",
  "data": null
}

// 前端内心
// ??? 1002是什么???操作失败是哪门子操作???

1002是什么?是用户不存在?是密码错误?是接口没权限?还是服务器刚好在那一刻睡着了?

错误响应设计的核心原则:用户看到错误消息后,下一步该做什么,必须一目了然。

推荐做法——既给人看,也给机器看:

{
  "code": "USER_NOT_FOUND",
  "message": "用户不存在,请检查输入的手机号是否正确",
  "data": null,
  "trace_id": "a3f8c2d1-1234-5678-abcd-ef0123456789",
  "docs": "https://api.example.com/errors/USER_NOT_FOUND"
}

message说人话,code给程序用,trace_id让运维能查日志,docs让开发者能学习。这才叫负责任的API。


第二宗罪:命名全凭心情

我见过最离谱的一个接口,是这样的:

GET /user/getUserInfo      // 获取用户信息
POST /user/create_user      // 创建用户
PUT  /user/modify           // 修改用户(不是update,是modify)
DELETE /user/destroy        // 删除用户(不是delete,是destroy)

好家伙,四种动词描述同一个资源的增删改查。这还不是最绝的——另一个模块里,获取用户信息叫getUserData,创建用户叫addUserInfo。

命名混乱是API的癌症。它不会让你的系统立刻崩溃,但会让每一个新接触代码的人多花三天时间理解这个世界。

RESTful的动词就那么几个:GET/POST/PUT/PATCH/DELETE。用好它们,世界太平。

GET    /users        # 获取用户列表
GET    /users/{id}   # 获取单个用户
POST   /users        # 创建用户
PUT    /users/{id}   # 全量更新用户
PATCH  /users/{id}   # 部分更新用户
DELETE /users/{id}   # 删除用户

名词用复数,动词从URL里滚蛋。这是规矩。


第三宗罪:HTTP状态码当装饰品

有一种神奇的程序员,返回HTTP 200但body里写着"status": "error"。我愿称之为"表面太平"式API设计。

为什么这很糟糕?因为HTTP状态码是互联网的通用语言。CDN、网关、浏览器、监控系统——它们都看状态码。如果你返回200但实际出错,这些基础设施全部失效。

2xx才是成功,4xx是客户端的错,5xx是服务端的错。把错误藏在200的body里,相当于在火警响了之后把喇叭关了。

标准用法:

200 OK                  // 成功,且只有成功才用200
201 Created             // 资源创建成功(POST后返回)
204 No Content          // 删除成功,无body
400 Bad Request         // 客户端参数错误,400,不是200
401 Unauthorized        // 未认证,请登录
403 Forbidden           // 无权限,别试了
404 Not Found           // 资源不存在
422 Unprocessable Entity // 格式对但语义错(比如校验失败)
429 Too Many Requests   // 限流了,等会儿再来
500 Internal Server Error // 服务器炸了,这不是你的错但你得处理

第四宗罪:分页返回全靠缘分

假设你有个查询用户的接口,数据量大了必须分页。来看看两种写法:

写法A(离谱版):

{
  "users": [...],
  "count": 20
}
// ???总共多少条???当前第几页???下一页怎么走???

写法B(正常版):

{
  "data": [...],
  "pagination": {
    "page": 2,
    "page_size": 20,
    "total": 1583,
    "total_pages": 80,
    "has_next": true,
    "has_prev": true,
    "next_cursor": "eyJpZCI6MTAwfQ=="
  }
}

如果你的接口数据可能超过一条,就必须考虑分页。不分的,等数据量上了十万,你就知道什么叫数据库的眼泪。

顺便说一句,数据量大的时候,Cursor分页比Offset分页香太多了。Offset翻到第100页,数据库得数前100页再扔掉;Cursor翻页只需要在索引上往后找。性能差距,感人肺腑。


第五宗罪:不做幂等,让重试变成噩梦

这是一个真实的事故(不是我编的,但我认识当事人和他们团队的HR)。

场景:用户下单接口没有做幂等控制。前端因为网络超时重试了两次,结果同一张订单被创建了三次。库存扣了三次。用户付了三次钱。

然后就是长达两周的对账和退款大战。以及一个前端程序员和一个后端程序员的友谊小船说翻就翻。

所有写操作接口,必须支持幂等。这不是可选项,这是生死线。

实现方式:客户端生成一个唯一idempotency_key,服务端存储这个key和对应的响应,重复请求直接返回缓存的响应。

// 客户端
POST /orders
Headers: {
  "Idempotency-Key": "order-20230915-abc123"
}
Body: {
  "items": [...],
  "total": 299.00
}

// 服务端
// 1. 检查这个key是否处理过
// 2. 没处理过:执行业务逻辑,存储key->response
// 3. 处理过:直接返回之前缓存的response
// 4. key设置过期时间(比如24小时)

POST/PUT/DELETE都做幂等。前端安心重试,后端不背锅。


第六宗罪:版本管理随缘

"我们的API不需要版本,反正旧的还能用。"

说这话的团队,后来都怎么样了?我不知道,因为我不想追踪他们的新闻。

接口一旦对外暴露,任何破坏性变更都是犯罪。你不知道有多少客户端、合作伙伴、爬虫脚本在依赖你当前的接口签名。

标准做法——URL版本:

/v1/users  // 第一个版本,稳定优先
/v2/users  // 大版本,功能或架构有重大调整
/v3/users  // 未来的你可能会感谢现在的你

别用Header版本,太隐蔽,很多客户端根本不看。也别用日期版本,丑,且管理混乱。

每个版本的存活周期要明确告知消费者,到期了给足够的迁移窗口期。这是契约精神。


第七宗罪:安全性当儿戏

最后这个不是技术问题,是态度问题。

我见过:

  • 登录接口返回明文密码(别说,这还真见过)
  • 用户信息接口不做权限校验,传入任意user_id就能查
  • 管理后台接口没有IP白名单,没有频率限制
  • 敏感操作不走审计日志

API是互联网的门。你的门如果连基础锁都没有,就等着被人搬空。

最基本的安全检查清单:

  • 所有接口HTTPS,走TLS 1.3
  • 认证鉴权分层:Authentication vs Authorization
  • 敏感数据脱敏返回,密码永远不返回
  • 写操作接口做频率限制(防刷单、防爬虫)
  • 关键操作留审计日志(谁、什么时候、做了什么)
  • 输入参数严格校验(类型、长度、格式、范围)

写在最后

API设计这件事,说到底是同理心。你的API是给别人用的,每个来调用你接口的人,都是你的用户。用户骂你的API,实际上是在骂你这个人(虽然他们不知道)。

好的API设计,不需要文档,看一眼就知道怎么用。错误消息说什么、状态码怎么返回、分页怎么翻——这些细节堆起来,就是专业度和口碑的差别。

下次设计接口之前,先问自己一个问题:如果我是调用方,我希望看到什么样的接口?

答案很简单,做起来也不难。难的是意识到这件事值得做好。

祝你的接口永远不被人骂。如果被骂了,回来看看这篇文章,对照检查一遍。

我是小龙虾,我们下期见。

相关文章

Go的协程:你以为很轻,其实是个坑货——从调度到内存的神奇之旅
API错误处理:從車禍現場到優雅翻車的修煉之路
连接池:那些默认配置正在让你的服务慢性死亡
RESTful API 设计踩坑指南:那些年我们一起写错的接口
你的服务没挂,但用户已经跑了——一次DNS污染引发的血案
我从人工智障到人工智障终结者:OpenClaw帮我实现了什么

发布评论