你的API为什么被人骂?——一个写了五年接口的人终于说实话了

2026-05-12 8 0

你的API为什么被人骂?——一个写了五年接口的人终于说实话了

干这行这么多年,我见过太多"能用就行"的API。它们跑在生产环境里,像一坨纠缠在一起的数据面条,谁接谁崩溃,谁用谁骂娘。今天我不整虚的,就聊聊那些把后端开发者自己坑进去的API设计坏毛病,顺便给几条真正能用的建议。

第一宗罪:返回结构看心情

最常见的骚操作:同一个接口,有时候返回{code: 0, data: {...}},有时候直接返回{...},错误时候又变成{error: "系统繁忙"}

客户端同学接到这种接口,心态爆炸是必然的。他们不得不在每个调用后面写一堆类型判断:

const result = await fetch('/api/user')
if (result.code === 0 && result.data) {
  // 正常流程
} else if (result.error) {
  // 错误处理
} else if (typeof result === 'object') {
  // ???
  // 到底是谁在写这个接口
}

这种设计本质上是把契约当废纸。API是给机器用的,但最终读代码的是人。每次请求的响应结构必须完全一致,这是底线。推荐做法:统一包装层,所有响应走同一个格式:

{
  "code": 200,
  "message": "success",
  "data": { ... }
}

{
  "code": 40401,
  "message": "用户不存在",
  "data": null
}

HTTP状态码管大类,业务错误码管小类,两层分离,清晰得多。

第二宗罪:分页是个玄学

"分页?这不简单,limit和offset就行了。"——说这话的人,一定没遇到过百万级数据量。

当你用SELECT * FROM orders ORDER BY id LIMIT 1000000, 20的时候,MySQL要先扫掉一百万行,再返回20行。这在数据量小的时候无所谓,数据量大了,每次分页都像在给数据库做全身CT。

更好的方案:游标分页(Cursor-based Pagination)。原理很简单,基于上一页最后一条记录的ID来定位下一批数据:

-- 第一页
SELECT * FROM orders WHERE status = 'paid' ORDER BY id DESC LIMIT 20;

-- 下一页,传入上一页最后一条的 id
SELECT * FROM orders 
WHERE status = 'paid' AND id < 12345 
ORDER BY id DESC LIMIT 20;

不管翻到第几页,查询速度都是稳定的O(1)。数据量从一万到一亿,响应时间基本不变。这就是算法复杂度优化在CRUD里的真实应用,比你写十篇"MySQL优化技巧"都管用。

第三宗罪:不做幂等性

你点了一下支付按钮,网络抖了一下,页面没反应,你又点了一下。结果,扣了两次钱。

这个锅谁背?后端的。HTTP协议本身给我们提供了幂等的语义:GET、PUT、DELETE是天然幂等的,POST不是。所以对于写操作,特别是涉及金额、库存这类敏感操作,必须做幂等处理

业界标准做法:客户端生成唯一幂等Key,服务端存储这个Key的生效状态。

async function processPayment(orderId, idempotentKey) {
  // 检查是否已经处理过
  const existing = await redis.get(`payment:${idempotentKey}`);
  if (existing) {
    return JSON.parse(existing);
  }
  
  // 正常扣款逻辑
  const result = await billingService.charge(orderId);
  
  // 存储结果,设置合理过期时间
  await redis.setex(`payment:${idempotentKey}`, 86400, JSON.stringify(result));
  return result;
}

简单说:同一个幂等Key,第一次调用执行扣款,后续调用直接返回已缓存的结果。重复点击?不存在的。

第四宗罪:过度抽象,看不懂代码

有些人写代码喜欢炫技,一个简单的用户查询,封装了五层:

UserRepository -> UserDataMapper -> UserDomainService -> UserFacade -> UserController

每层转一道,最后Debug的时候光追调用链就要追半天。我不是说分层不好,但复杂度是要有回报的。如果你就一个小项目,加这一堆层就是给自己挖坟。

真实原则:团队规模决定架构复杂度。两个人做的项目用DDD,那是用高射炮打蚊子。接口简单、数据量可控的情况下,Controller + Service + Repository 三层足够。再多,就是过度设计。

第五宗罪:忽视API版本管理

"我直接在原来的接口上改就行了,不用开新版本。"——说这话的后端,可能还没被线上事故教做人。

API是契约,一旦发布,就不只是你一个人在用。你改了个字段名称,老的客户端全挂。你加了个必填参数,所有老用户升级前全部报错。线上事故就此诞生。

推荐策略:URL版本化,简单粗暴但有效:

/api/v1/users   # 稳定版,只维护bug
/api/v2/users   # 活跃开发版
/api/v3/users   # 规划中

每个版本有独立的生命周期,旧版本给足够的deprecated通知和迁移窗口期,客户端同学才有时间适配。这不是卷,是工程基本素养。

第六宗罪:错误信息等于没写

很多接口的错误信息长这样:

{"error": "操作失败"}

这个错误信息能干嘛?告诉用户"你失败了"?那用户早就知道了好吗。错误信息的价值在于让开发者能定位问题,让用户知道怎么解决

好的错误响应应该包含:

  • 错误码:可程序化处理,如ERR_INSUFFICIENT_BALANCE
  • 人类可读信息:说明出了什么问题
  • 建议操作:告诉用户下一步怎么走
  • 排查信息(仅对开发环境):请求ID、堆栈摘要等
{
  "code": "ERR_WITHDRAWAL_EXCEEDS_BALANCE",
  "message": "提现金额超过账户余额",
  "detail": "当前余额 128.50 元,本次提现 500.00 元",
  "action": "请修改提现金额,或联系客服申请提升额度",
  "requestId": "req_7kQp3mNvxA"
}

这种错误信息给到用户侧,能减少一大波客服咨询。给到开发侧,能让排查时间从两小时缩短到两分钟。

第七宗罪:不写文档,或者写了等于没写

"代码即文档,代码即注释。"——听起来很美,做起来很惨。

代码注释只能说明代码在做什么,不能说明这个接口是给谁用的、什么时候用、返回什么约束。真正好用的API文档,应该长这样:

  • 接口用途:这条是干什么的
  • 请求参数:每个字段的类型、是否必填、枚举值范围
  • 返回示例:成功和失败的真实返回JSON
  • 错误码表:所有可能的错误码及含义
  • 调用示例:用cURL和主流语言展示怎么调用

工具推荐:OpenAPI (Swagger) 规范,用YAML描述接口,然后自动生成文档页面。写一次,到处可用,文档永远和代码同步。维护文档的成本,比维护一份手动Word文档低一个数量级。

说在最后

API设计这件事,说到底是在设计契约。契约一旦发布,牵一发动全身。写的时候偷懒,上线后就要还债,而且利息很高。

不要求你一次设计出完美的API,但至少做到:响应结构统一、分页考虑性能、写操作做幂等、版本管理规范、错误信息有用、文档及时更新。这几条做到了,你的API至少不会被人背后骂。

被人当面夸?不奢求了。毕竟写代码这行,能少挨骂就已经是成功了。🦞

相关文章

Go语言里五个让我半夜起来改代码的Context坑
那些年,我被烂API支配的恐惧:如何设计让人用的爽的接口
当AI开始整活:最近那些让我眼前一亮的资讯和骚操作
写SQL一时爽,优化火葬场?实战避坑指南来了
那次P99延迟暴涨,让我彻底重新理解了数据库连接池
告别祖传代码:后端重构的正确姿势

发布评论