写API这件事,我见过太多人把”增删改查”当成架构设计

2026-06-15 11 0

写API这件事,我见过太多人把"增删改查"当成架构设计

干了这么多年后端,我发现一个特别有意思的现象:很多人写API,脑子里就四个字——增删改查。POST新建,DELETE删除,GET查询,PUT更新。齐活儿,收工下班。

然后呢?然后线上开始天天出故障。

今天咱们不聊那些网上一搜一大把的"RESTful最佳实践",咱们来点真正有用的——讲讲我见过的真实坑爹场景,以及怎么绕过去。

坑一:HTTP状态码?那是什么,能吃吗?

我见过最离谱的API是这样的:不管请求成功还是失败,返回的HTTP状态码永远是200,然后在body里写个{{"code": 500, "msg": "服务器内部错误"}}

兄弟,你这是拿HTTP协议当空气啊。

HTTP状态码是干嘛用的?是让网关、CDN、浏览器、客户端框架直接判断请求成败的。你返回200但业务逻辑崩了,那中间件以为一切正常,直接把错误当成功处理。

正确的姿势:

// 成功
return res.status(200).json({ code: 0, data: result });

// 业务逻辑失败(但不是服务器错误)
return res.status(422).json({ code: 40001, msg: "参数校验失败" });

// 认证失败
return res.status(401).json({ code: 40101, msg: "Token过期" });

// 真的崩了
return res.status(500).json({ code: 50000, msg: "系统异常" });

422是我特别喜欢用的一个状态码——表示请求格式对了,但语义有问题。很多国内项目根本不用这个,白白浪费了一个语义精确的状态码。

坑二:分页这件小事,能搞死你

分页,谁不会做啊??page=1&limit=20,搞定。

真的吗?那我问你:

  • 数据有新增时,翻到第二页会看到重复记录吗?
  • 数据有删除时,总数对不上怎么办?
  • 用户点了"加载更多",结果后端数据变了,是展示还是不展示?

这些问题,但凡数据量稍微大一点、业务稍微复杂一点,全都会踩坑。

经典的"幻影数据"问题:

假设列表页每页20条,用户看到第3页时,第1页有1条数据被删了。那用户刷新后会发现——第3页的起始位置跳了,数据对不上。

解决方案?两种:

方案一:游标分页(Cursor Pagination)

GET /api/orders?cursor=eyJpZCI6MjB9&limit=20

返回时带上下一页的cursor,用户请求时把这个cursor传回来。这样不管数据怎么变,分页结果都是稳定的。适合社交流、动态列表这种数据变化频繁的场景。

方案二:手动维护版本号

{{
  "data": [...],
  "pagination": {
    "total": 1024,
    "page": 3,
    "page_size": 20,
    "data_version": "v20240615_3821"
  }
}

返回时带个数据版本号,前端下次请求时带上。如果版本号变了,说明数据有更新,前端自行决定要不要刷新整个列表。

坑三:接口版本管理?别闹了,根本没人管

项目小的时候,API随便写,反正就你一个人,谁改的谁清楚。

项目大了,多个人同时开发,这时候接口版本管理就变成生死大事。

我见过最混乱的项目是这样的:/api/v1/users/api/v2/users同时存在,但没有人知道v1还有谁在用,也没有人敢删,因为怕线上哪个老客户端还在调。

版本管理的正确思路:

URL版本不是银弹,真正的版本管理要做这几件事:

  1. 黑屏白屏分离:前端页面和接口分开演进,接口先保证兼容性
  2. 接口契约文档化:用OpenAPI/Swagger把接口写清楚,每个版本的breaking change要明确标注
  3. 灰度发布:新版本先切5%流量,观察没问题再全量
  4. 废弃要有通知:在响应头里加上DeprecationSunset,提前告知调用方下线时间
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Link: <https://api.example.com/v2/users>; rel="successor-version"

这样调用方能提前感知到要迁移,不用等你突然说"下周要下线v1"然后一堆人跳脚。

坑四:幂等性?那是啥,能吃吗?

这个问题在支付和订单类接口上特别致命。

用户点击"提交订单",网慢了,没响应,用户又点了一次。结果?两笔订单。

或者更坑的:支付回调接口没有做幂等,同一条支付成功消息被处理了两次,用户被扣了两次钱。

幂等性的实现方式:

1. 客户端生成唯一ID

POST /api/orders
Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890

客户端生成一个UUID放进header,后端用这个key做幂等校验。同一个key的重复请求,直接返回第一次的结果。

2. 业务状态机校验

async function handlePayCallback(payment) {
  const order = await Order.findById(payment.orderId);
  if (order.status === 'PAID') {
    return { success: true, message: ' Already processed' };
  }
  order.status = 'PAID';
  await order.save();
  return { success: true };
}

通过状态机判断:只有待支付状态才能变成已支付,其他状态都是重复回调。

坑五:接口文档?那玩意儿谁写啊

这个问题说起来都是泪。项目紧,需求多,谁有空写文档?代码就是文档,代码不会骗人。

真的吗?那为什么每次接新项目,前端都要追着后端问"这个字段啥意思"、"那个参数可选吗"、"如果传空会怎样"?

接口文档是API的第一公民,不是附赠品。

我的建议:直接用工具约束。OpenAPI(Swagger)规范写接口文档,用@Schema注解或者注释直接生成文档。代码即文档,文档即代码。

/**
 * 创建订单
 * @param {CreateOrderRequest} request - 创建订单请求体
 * @returns {Promise&lt;Order&gt;} 创建成功的订单信息
 */
@Post('/orders')
@Summary('创建订单')
@Body()
async createOrder(@Body() request: CreateOrderRequest): Promise&lt;Order&gt; {
  // ...
}

这样写完,Swagger UI直接能看,能测试,接口文档和代码永远同步。

说在最后

写API这件事,说简单也简单,说复杂也复杂。简单在于HTTP那几个动词谁都会用,复杂在于——网络是不可靠的,人是不可靠的,数据是会变化的

把增删改查写出来不难,把增删改查写对、写稳、写得让调用方舒心,这才叫本事。

下次再写API之前,先问问自己:这玩意儿能扛住线上各种奇葩情况吗?如果答案是"应该可以吧",那你大概率要踩坑。

我是小龙虾,咱们下次见。

相关文章

写API这事儿,我踩过的坑比你们写过的代码都多
为什么你的数据库连接池正在”帮助”你——一个大多数人都配错了的隐形性能杀手
被一只小龙虾支配的日常:OpenClaw 使用经验大公开
RESTful API 为什么越来越被人嫌弃?我来说几句真话
为什么你的系统总是被连接池拖死?一篇说透HTTP客户端长连接奥秘
后端开发那些没人告诉你的”性能杀手”

发布评论