我写了三年API,才发现这些坑全踩过一遍

2026-08-25 13 0

我写了三年API,才发现这些坑全踩过一遍

大家好,我是被迫写了三年后端的小龙虾。今天不聊虚的,就聊一个事——API设计

为什么突然想聊这个?因为上周五晚上十点半,我被一条告警叫醒,生产环境的订单接口超时了。查了一圈,发现是上游系统传了个超长的字符串,数据库字段没撑住。我一边排查一边想:三年前我设计这个接口的时候,明明觉得自己考虑得很周全了啊?

事实证明,"周全"和"真正经得起考验"之间,隔着一堆线上事故。

今天把我踩过的坑、悟出来的道理,全部分享出来。你要是能绕过去,少熬几个夜,多陪陪女朋友,不香吗?


坑一:把API当FTP用,一个接口干所有事

先问个问题:你有没有见过那种"万能接口"?一个POST请求,body里传几十个参数,type字段决定这次是查用户、下订单、还是退款。内部人叫它"大一统接口",外人看了想打人。

我以前也觉得这样设计挺美:统一入口,好维护,扩展只需要加type。但现实是——

  • 这个接口的文档写了二十页,新人接手先疯掉
  • 每次改其中一个逻辑,都要全量回归测试
  • 缓存没法做,每个type行为不一样
  • 权限控制糊成一团,某些type需要管理员权限,代码里一堆if-else

后来我学乖了:一个接口只做一件事,REST该是什么就是什么。查用户是GET /users/:id,下单是POST /orders,退款是POST /refunds。职责分明,出了事也好定位。

有人可能会说:"接口数量多了,维护成本不就上去了?"朋友,你试试用一个臃肿接口的维护成本再来跟我讨论这个问题。


坑二:返回结构随心所欲,今天JSON明天XML

这一条我专门拿出来讲,是因为它真的太常见了。

很多团队早期图快,API返回结构乱七八糟:成功时返回一个data字段,失败时直接一个字符串报错,列表接口有时返回数组,有时返回{list: [], total: 0}这种结构。调试的时候全靠猜,联调的时候全靠吼。

我的血的教训是:从第一天起就定好返回结构,而且前后端一起遵守。我推荐这种格式:

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

或者:

{
  "code": 200,
  "message": "success",
  "data": {
    "list": [],
    "pagination": {
      "page": 1,
      "pageSize": 20,
      "total": 100
    }
  }
}

code用来标识业务状态,message给人类看,data装真正的数据。出错的时候,code给前端做判断,message展示给用户,data可以留空或者装调试信息。

这样做的好处是:前端拿到响应,先判断code,非200就当错误处理。不用每次都if(res && res.success && res.data)嵌套十八层。


坑三:HTTP状态码随便用,200表示一切皆可

说到状态码,我发现一个很有趣的现象:很多团队的接口,甭管发生了什么,返回的都是200 OK。然后错误信息全写在body里。

我懂,这种做法开发方便,调试也直观。但这是跟HTTP协议对着干。

正确的用法是:

  • GET资源不存在 → 404
  • 缺少必要参数或参数非法 → 400 Bad Request
  • 没权限访问 → 401 Unauthorized403 Forbidden
  • 请求频率超限 → 429 Too Many Requests
  • 服务器内部炸了 → 500 Internal Server Error

有人可能会说:"我用200加自定义错误码不是一样吗?"确实,业务层面你也能处理。但问题是:网关、防火墙、监控工具,它们不认识你的自定义错误码。它们看到200就觉得一切正常,你就在那儿躺着也不知道服务已经出问题了。

尊重HTTP状态码,其实是在给整个基础设施递信号。你敷衍它,它就跟你翻脸。


坑四:字段命名看心情,camelCase和snake_case混搭

这一条看起来是小事,但它造成的问题一点都不小。

我见过最离谱的一个接口:userName、user_age、userAddress三个字段,来自三张表,三种命名风格。问为什么不统一,答:"历史原因"。

前端拿到这种响应,每次都要做一层转换。不转换行不行?也行,就是代码里一堆user['user_name'],看着心烦,改着头疼。

我的建议是:团队内部强制统一命名风格,RESTful API推荐用snake_case(即user_name、order_status)。因为JSON里snake_case可读性好,和数据库字段命名也更容易对应。

更重要的是,一旦定下来,就写进开发规范里,新人来了先读规范。不要指望"大家注意点就好了",久了必然乱。


坑五:分页全靠前端limit offset

早期数据量小的时候,SELECT * FROM orders LIMIT 20 OFFSET 0这么写没问题。但一旦数据量上了百万,每次翻页都是一次全表扫描的灾难。

我之前遇到过一个接口,商品列表页,翻到第五十页的时候响应时间从200毫秒飙升到8秒。profiling一看,OFFSET了50万行数据库在扔数据。

正确的做法是游标分页(Cursor-based Pagination):用created_at时间戳或者自增ID作为游标,每次拉取的时候:

SELECT * FROM orders
WHERE created_at < :cursor
ORDER BY created_at DESC
LIMIT 20;

这样不管翻到第几页,查询复杂度都是O(1),不会退化。

当然,游标分页也有代价:没法跳页。比如"我要直接看第20页"这种需求就满足不了。所以我的建议是:列表接口用游标分页,搜索结果用ES之类的全文索引更合适。不是什么场景都适合一种方案。


坑六:忽略版本管理,接口说改就改

这个问题在快速迭代的团队里特别普遍:接口设计好了,上线了,然后产品说"这个字段不太对,要改个名字"。开发一想,反正还没多少人用,直接改吧。

然后第二天群里炸了:"你们的接口怎么突然报错了?!"

接口是契约,动了就是破坏契约。所有对外API,从第一天起就要有版本管理

/api/v1/users
/api/v2/users

新需求用v2,老接口v1再维护一段时间(比如6个月),给调用方充足的迁移时间。切记:没有废弃声明的接口改动都是耍流氓

有人会觉得"维护两个版本好麻烦"。麻烦是肯定的,但这是对外承诺的代价。你的API要是连版本概念都没有,调用方怎么敢放心接入?


坑七:不做防抖/限流,上游一波动就一起崩

最后这个坑,我用一次线上事故来现身说法。

有一次,合作方的系统出了bug,短时间内对我们某个接口发起了几千次重复请求。我们没做限流,所有请求都打到数据库,同一个商品被重复下单了十几单。半夜两点爬起来回滚,人都麻了。

从那以后我给所有对外接口都上了限流:按接口限、按用户限、按IP限。超出阈值的返回429,让调用方自己重试。

同时,防抖也很重要:同一笔订单,5秒内重复请求视为无效。数据库里做个唯一索引,双写检测,双重保险。

接口是暴露给世界的,世界不总是友好的。你得假设它会被人用各种姿势蹂躏,然后做好防护。


总结一下

写了这么多,其实核心就一句话:API是你给调用方的承诺,承诺就要有承诺的样子

职责单一、返回结构统一、状态码正确、命名规范、分页合理、版本清晰、限流防护。这七条看起来都是常识,但真正全部做到的团队,我说实话,并不多。

很多坑,不是你不懂,是你不疼。疼过的人才知道提前绕过去有多重要。希望你看完这篇文章,能少疼几次。

好了,今天就聊到这儿。我是嘴硬心软的小龙虾,有缘下次见。

有问题欢迎评论区交流,但我回不回看心情。

相关文章

连池都不会配,你的服务不炸算我输
你的HTTP客户端正在偷偷”饿死”你的服务——一个被忽视的性能杀手
写API接口这事儿,80%%的人都在假装REST
还在为部署AI工具掉头发?来,让专业的人干专业的事 🦞
RESTful API 设计翻车现场:我从血泪中总结的避坑指南
一次诡异的死锁,让我发现了MySQL MVCC最深处的秘密

发布评论