API设计翻车实录:那些年我们一起踩过的坑

2026-07-03 4 0

API设计翻车实录:那些年我们一起踩过的坑

干后端开发的,谁没在API设计上翻过车呢?我见过最离谱的一个接口,返回一个用户信息居然要调三次接口——第一次拿基本信息,第二次拿扩展信息,第三次拿所谓的「安全信息」。客户端同学调接口调到怀疑人生,最后直接在前端写了三个请求并行发,代码看起来像是在召唤神龙。

今天不整虚的,来聊聊API设计中那些让人血压飙升的坑,以及怎么优雅地绕过去。

一、RESTful不是银弹,别为了形式丢了实用性

2010年代那会儿,RESTful API几乎成了「专业」的代名词。所有人都说要用名词不用动词,要用HTTP方法语义,结果呢?

一个订单系统里,你需要:

GET    /orders           # 查订单列表
POST   /orders           # 创建订单
GET    /orders/{id}      # 查单个订单
PUT    /orders/{id}      # 更新订单
DELETE /orders/{id}      # 删除订单
PATCH  /orders/{id}/cancel  # 取消订单...等等,这还是RESTful吗?

到了「取消订单」这个操作,RESTful的纯粹性就开始碎裂了。有人坚持用PATCH把整个订单状态位改掉,有人偷偷加了/action/Cancel这样的动词尾巴,有人干脆开了个/webhooks/resolver这种听起来就很敷衍的接口。

我的观点是:RESTful是个好思想,但别让它绑架了你的业务。有些操作天然就是动宾结构,强扭的瓜不甜,强行RESTful只会让接口看起来像是在写八股文。

实战建议:简单场景用REST,复杂业务逻辑用RPC风格也完全没问题。GraphQL和gRPC也是工具,不是信仰。

二、分页不是简单limit offset,能不用就别用

最常见的分页实现:

GET /users?page=1&page_size=20

背后的SQL大概是这样:

SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 20;

数据少的时候没问题,数据多了试试?offset到十万级别的时候,数据库得先把前十万行给你「扔掉」,这个扔掉的动作可是实打实要扫描的。

更好的方案是什么?游标分页(Cursor-based Pagination)。不再说「给我第5页」,而是说「给我id大于1000之后的20条」。

GET /users?after=1000&limit=20
# 返回 users 数组 + next_cursor 字段

SQL变成:

SELECT * FROM users WHERE id > 1000 ORDER BY id LIMIT 20;

不管数据有多少万条,这个查询永远是索引范围内的高效扫描,时间复杂度O(log n)稳定输出。朋友圈、微博、抖音的feed流,全都是游标分页,你以为他们用的是page参数?天真了。

三、错误处理:你在返回200但其实已经错了

这是最让人无语的反模式:

HTTP/1.1 200 OK
{
  "success": false,
  "error": {
    "code": 10001,
    "message": "余额不足"
  },
  "data": null
}

HTTP状态码是200,但业务上已经报错了。客户端得先看你success字段才知道成没成功,这不是脱了裤子放屁吗?

正确做法:

HTTP/1.1 402 Payment Required
{
  "error": {
    "code": 10001,
    "message": "余额不足"
  }
}

把HTTP状态码用起来,这是协议层给我们的资产:

  • 400:客户端问题(参数错误、格式不对)
  • 401:未认证
  • 403:已认证但没权限
  • 404:资源不存在
  • 422:语义正确但业务不允许(比如余额不足这种「逻辑错误」)
  • 429:请求过于频繁
  • 500:服务端故障

有人会说422不太常见,浏览器不太认识。但你的API是给程序调用的,不是给浏览器看的,程序在乎的是语义清晰,不是浏览器认不认识这个状态码。

四、版本管理:URL版本号才是yyds

API版本管理的策略主要有三种:

1. URL路径版本(最常见):GET /api/v1/users

2. Header版本:API-Version: 2023-01-01

3. 查询参数版本:GET /api/users?version=1

我见过用Header版本的团队,调试接口的时候得用Postman手动加Header,打日志的时候得专门记录是哪个版本。curl测试还得记得每次都加-H "API-Version: xxx",多了以后测试同学恨不得把电脑砸了。

URL版本号的最大优势是什么?可见性。日志里一清二楚哪个版本的接口被调了,缓存粒度可以精细到版本级别,API文档里不同版本的接口泾渭分明。

至于URL里带版本号「不RESTful」这种说法——前面说过了,别让形式大于内容。

五、批量接口:你真的需要N+1吗?

假设前端要展示一个仪表盘,需要用户信息、订单统计、待处理任务三个数据块。

一个没经验的实现:

GET /current_user
GET /orders/stats
GET /tasks/pending

三个请求串行,假设每个200ms,用户得等600ms才能看到完整页面。如果搞成并行,虽然快了一倍,但三个请求各自走一遍网络开销和连接建立时间,总耗时还是在300ms以上。

更好的设计:

GET /dashboard
# 返回:
{
  "user": {...},
  "order_stats": {...},
  "pending_tasks": [...],
  "generated_at": "timestamp"
}

一次请求,后端做数据聚合,前端拿到就是完整的。而且后端可以对这些数据做本地缓存,数据库查询也能复用同一条连接,减少连接建立的开销。

写在最后

API设计这件事,说到底是给调用方的一份承诺。你设计的每一个接口,都在定义别人使用你系统的方式。接口烂,调用方就痛苦;接口好,调用方写代码就是享受。

别追求花里胡哨的概念,把握住几个核心:语义清晰、分页合理、状态码正确、版本管理清晰、少做N+1。做到这几点,你的API至少不会让人想砸键盘。

当然,最重要的还是——写接口之前先问问自己:如果我是调用方,我会想怎么用?有时候换位思考一下,比看十篇架构论文都有用。

相关文章

API设计翻车实录:那些年我们一起踩过的坑
为什么你的服务挂了,你却是最后一个知道的?——我见过最被低估的后端技能
为什么你的服务挂了,你却是最后一个知道的?——我见过最被低估的后端技能
写API这两年:我是如何从”能用就行”进化到”这API真优雅”的
RESTful API设计:那些年我们一起踩过的坑
我在生产环境用Docker跑数据库,被leader当场骂了一顿

发布评论