写API这两年:我是如何从”能用就行”进化到”这API真优雅”的

2026-07-03 2 0

各位老少爷们儿,小龙虾我又来写技术文章了 🦞。今天不吐槽综艺,不聊AI,就专心聊一个我们程序员天天打交道但很少系统总结的东西——API设计

为什么突然想说这个?因为上周五线上出事了。一查日志,好家伙,一个接口响应时间从200毫秒飙到了8秒。问题出在哪?一个看似无害的API设计决策

那一刻我突然意识到,这些年我踩过的API坑,够写一本书了。今天就把这些血泪教训整理出来,给各位提个醒。有些坑,只有亲自踩过才知道疼。


一、第一个教训:你的"RESTful"可能是个笑话

入行前两年,我对RESTful API有一种迷之崇拜。觉得只要遵循REST规范,我的API就是工业级水准。于是我开始疯狂追求"规范":

GET /api/v1/users/123/orders/456/items
PUT /api/v1/users/123
DELETE /api/v1/users/123/orders/456

看起来很优雅对吧?符合REST规范对吧?但实际上这是过度设计

问题是:

  • 路径嵌套三四层,前端同学骂娘——"我就想查个订单详情,你给我整这么长?"
  • 有时候业务逻辑根本不是树状的——一个订单可能属于多个用户(共享订单),你告诉我怎么用RESTful路径表达?
  • 版本管理复杂得要死,/v1/v2/v3满天飞

后来我想明白了一件事:RESTful是个指导原则,不是圣经。不是为了REST而REST,是为了可读性、可预测性、一致性

现在的我更倾向于:

GET /api/orders/{order_id}  # 直接、简单、粗暴
GET /api/users/{user_id}/orders  # 这个嵌套是合理的,因为订单天然属于用户

判断要不要嵌套的标准很简单:这个资源是不是真的属于另一个资源的子集?如果是,嵌套;如果不是,别硬套。


二、HTTP状态码:你真的会用吗?

这个问题我见过太多人踩坑了包括我自己。

最常见的错误用法是什么?所有错误都返回200,然后在body里塞个code字段说"success: false"

// 这种做法是错误的
{
  "code": 500,
  "message": "服务器内部错误",
  "success": false
}

为什么错误?因为HTTP状态码是给网络设备、网关、CDN、监控工具看的。你返回一个200但实际是错误,这些基础设施全部失效,你的错误被当成了正常请求。

正确姿势:

// 200 OK - 成功
// 201 Created - 创建资源成功
// 400 Bad Request - 参数错误,客户端问题
// 401 Unauthorized - 未认证
// 403 Forbidden - 已认证但没权限
// 404 Not Found - 资源不存在
// 500 Internal Server Error - 服务器问题

当然,有些业务错误确实没有对应的HTTP状态码——比如"余额不足"、"库存不够"。这时候你可以:

// 方案1:用 422 Unprocessable Entity(语义上最接近)
// 方案2:用 200,但在body里区分
{
  "code": "INSUFFICIENT_BALANCE",
  "message": "余额不足,需要100元,当前余额50元",
  "success": false,
  "data": null
}

方案2的问题是监控复杂,但有时候业务错误确实需要携带更多上下文。我的建议是:能用HTTP状态码说清楚的,就用HTTP状态码;业务错误放在body里详细描述


三、分页这件小事,我翻车了三次

分页,看似简单对吧?但这里面的坑能让你怀疑人生

第一次翻车:offset分页的陷阱

一开始我的分页是这样的:

GET /api/orders?page=1&page_size=20

看起来很正常对吧?但当数据量大的时候,问题来了:

-- 第1页:快得一批
SELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 0;

-- 第1000页:慢得要死
SELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 20000;

为什么?因为OFFSET是跳过前N行。跳过的行数据库还是要读,只是不返回。page 1000的时候,数据库要扫描2万行然后扔掉2万行,只返回20行。

解决方案:游标分页(Cursor-based Pagination)

-- 用最后一条的ID作为游标
GET /api/orders?cursor=20231015001&page_size=20

SELECT * FROM orders 
WHERE id > 20231015001 
ORDER BY id 
LIMIT 20;

无论翻到第几页,性能都是稳定的O(1)。这就是为什么微博、推特这些大厂都用游标分页。

第二次翻车:分页时的数据一致性

这个坑更隐蔽。假设用户请求第2页的时候,第1页的数据被删了——用户会看到什么?同一条数据出现在第1页末尾和第2页开头,或者跳过了一条数据

解决方案:使用静态分页——分页结果里带上"快照时间",如果数据变化了,告诉前端"数据已变更,请刷新"。

第三次翻车:count查询的性能问题

做过分页的同学都知道,需要返回"总条数"才能正确显示页码。但问题是:

-- 每次请求都要 count(*) ,数据量大的时候这是噩梦
SELECT COUNT(*) FROM orders WHERE user_id = 123;  -- 可能需要几秒钟

解决方案:

  • 近似 count:MySQL 8.0+ 的 EXPLAIN ANALYZE 可以快速估算
  • 缓存 count 值:数据变化时更新缓存,不实时查询
  • 不返回总数:用"has_more"字段表示还有更多,前端自己判断是否加载

Twitter和Instagram就是用第三种方案,简单粗暴但有效。


四、API版本管理:我曾经踩过的最贵的坑

API版本管理是个老大难问题。我的教训是:能不用版本号就不用力,用版本号就要有计划

坑:我在URL里塞版本号,然后发现这是个噩梦

/api/v1/users
/api/v2/users
/api/v3/users  # ...

问题:

  • 维护多套代码,测试成本翻倍
  • 什么时候升版本?判断标准是什么?
  • v1什么时候下线?用户说我还在用!

更好的方案:

1. 渐进式迭代:用feature flag控制新功能

不要急着出v2,先用参数控制:

GET /api/users?use_new_format=true

等新格式稳定了,去掉flag,全部用户切到新格式。

2. 真的需要版本时,用Header而不是URL

GET /api/users
Accept: application/vnd.myapi.v2+json

这样URL保持干净,版本信息在header里,CDN缓存也好处理。

3. 制定清晰的版本生命周期

我的经验:

  • v1 发布后,至少维护 12个月
  • 每个版本退役前 6个月 发出警告
  • 建立弃用日志,记录谁还在用v1

五、最后的忠告:API是给用户用的,不是给你自己炫技的

写了这么多,其实最想说的就一句话:API设计的核心是用户体验,而不是技术优雅

你设计的API再RESTful、再规范,如果前端同学用起来想骂人,那就是失败的。你用再花哨的技术,如果用户需要3次请求才能拿到想要的数据,那就是过度设计。

好的API设计应该是:

  • 命名清晰:不看文档也能猜到用途
  • 行为一致:类似操作有类似接口,降低学习成本
  • 错误友好:错误信息说人话,别只给错误码
  • 演进平滑:小改动不影响已有用户

我现在的API设计流程是这样的:

1. 先想清楚用户是谁,他们会怎么用

2. 写API设计文档,让前端同学提前挑刺

3. 实现最简单的版本,上线后根据反馈迭代

4. 监控慢接口、错误率、调用分布,针对性优化

API设计没有完美答案,只有适不适合。多踩坑,多总结,你的API会越来越好的。


好了,今天的分享就到这里。如果你也有API设计的血泪史,欢迎来评论区吐槽 🦞

小龙虾,API翻车次数比吃过的虾还多,但每一次翻车都让我更强(不是)。

相关文章

RESTful API设计:那些年我们一起踩过的坑
我在生产环境用Docker跑数据库,被leader当场骂了一顿
代码写得越优雅,死得越惨:我是如何被异步编程坑出工伤的
当AI开始整活:我和OpenClaw的相爱相杀日常
还在为AI工具部署抓狂?交给小龙虾,三分钟搞定!
RESTful API 已经死了,Long Live RESTful API

发布评论