RESTful已死:为什么你在浪费生命设计"正确"的API
我见过太多团队为了追求"RESTful规范"吵得面红耳赤,GraphQL和gRPC之间互相鄙视,OpenAPI文档写得比代码还长,但没人说得清楚这一切到底为了什么。
今天我要说一句很多人不敢说的话:你花大量时间设计的"完美API",可能从一开始就是错的。
一个令人不安的事实
大多数后端开发者学习API设计时,第一件事就是背REST原则:资源、动词、状态码。然后呢?然后他们花了大量时间争论PUT和PATCH的区别,纠结应该用201还是200,研究HTTP缓存头怎么配。
这些重要吗?某种程度上。但更重要的是:你的API是给谁用的?
如果你的API主要给自家前端用,那你追求的"规范"可能全是无效工作。如果你的API是公开API,那暴露的内部实现细节可能在给自己挖坑。
实战教训:一场"规范化"引发的血案
我曾经参与过一个项目,团队对API规范性有着近乎强迫症的追求:
- 所有列表接口必须支持分页、排序、筛选
- 所有更新操作必须区分PUT(完整更新)和PATCH(部分更新)
- 所有删除操作必须返回204状态码
- 所有错误必须遵循RFC 7807规范
听起来很美好,对吧?但现实是:
- 前端团队抱怨接口响应慢——因为后端花太多时间处理那些"规范化"逻辑
- 移动端团队直接绕过这些接口,自己写了一套简化版——因为规范太复杂根本用不上
- 文档团队维护着一套比代码还难懂的OpenAPI规范,里面充斥着永远不会被调用的端点
最讽刺的是什么?这个API 90%的调用都来自内部,前端要什么后端早就知道了。那套"完美设计"纯粹是给自己看的表演。
你的API使用者不是机器人
很多人在设计API时陷入一个误区:把API当成数学公式,恨不得用最少的端点、最抽象的资源模型覆盖所有场景。
但API是给人用的,不是给代码静态分析工具用的。
举个例子。假设你在做一个电商系统,传统做法可能是这样:
GET /api/products?category=electronics&min_price=1000&max_price=5000&sort=price&order=asc&page=1&page_size=20
URL长得像一坨屎,但这是"规范"的。
另一种做法:
POST /api/products/search
{
"category": "electronics",
"price_range": [1000, 5000],
"sort": "price_asc",
"page": 1
}
哪种更好?如果你做的是内部系统,第二种明显更实用:URL参数少了好维护,请求体可以加注释,而且将来扩展字段也方便。
如果你做的是公开API,需要给第三方集成,第一种更合适——符合"可发现性"原则。
问题是:很多人在做内部系统时也在用第一种方案,因为他们觉得这样"更规范"。
gRPC vs REST:不是一个层面的战争
每隔一段时间,就有人跳出来说REST已死,gRPC才是未来。然后另一群人反驳说gRPC太复杂,REST才是正道。
我只想问一句:你们争的是技术,还是自己的技术优越感?
真实情况是:
- REST:适合浏览器优先、需要广泛兼容性的场景。调试简单,生态成熟。
- gRPC:适合微服务间高性能通信、强类型契约优先的场景。支持双向流。
- GraphQL:适合多端(Web/iOS/Android)需要不同数据形状的场景。代价是复杂度上升。
- WebSocket:适合实时性要求高的场景。不是替代品,是不同场景的工具。
没有银弹。我见过把gRPC用在一台机器上两个进程间通信的团队,也见过在需要实时推送的场景坚持用REST polling的团队。
选型的标准只有一个:你的场景真正需要什么?
版本管理:被神化的最佳实践
几乎所有API设计教程都会告诉你:必须做版本管理,URL里必须有/v1/或者/v2/。
但我要告诉你一个反直觉的事实:URL版本控制可能是你最不需要担心的。
看看GitHub API:
GET https://api.github.com/gists/public
没有版本号。GitHub怎么做到的?靠的是强大的向后兼容性。他们宁可维护旧字段,也不轻易破坏现有调用。
当然,GitHub是顶级公司,不是每个团队都能做到。那种情况下该怎么办?
我的建议:
- 内部系统:优先做好向后兼容,用header做隐式版本控制就够了
- 公开API:如果必须显式版本,先评估维护成本,能用小版本解决的问题不要动不动就大版本升级
- 无论什么系统:废弃策略比版本号更重要。你的旧接口打算保留多久?给调用方多少迁移时间?这些才值得花时间设计
错误处理:最容易露馅的地方
如果让我用一个指标判断一个团队API设计水平,我会看他们的错误处理。
烂的API的错误处理长这样:
{
"code": 1001,
"message": "操作失败",
"data": null
}
这个"1001"是什么鬼?客户端开发者怎么知道1001对应的是什么错误?
好的API错误处理长这样:
{
"error": {
"code": "INSUFFICIENT_INVENTORY",
"message": "商品库存不足,当前可用数量:3",
"details": {
"product_id": "SKU-2024-001",
"requested": 10,
"available": 3
},
"trace_id": "req_abc123xyz"
}
}
关键要素:
- 错误码是人类可读的字符串,不是数字。调试时搜错误码比搜数字方便一百倍。
- 消息要给人类看,包含足够上下文。
- details是给开发者看的结构化数据,客户端代码可以直接拿来用。
- trace_id用于链路追踪,出问题了你才知道去哪查日志。
好的错误处理是给调用方省时间的,而不是展示你有多严谨。
写在最后
API设计的本质是什么?是让调用方用最小的认知负担完成任务。
不是什么"符合HTTP语义",不是什么"RESTful六大约束",不是什么"GraphQL才是现代化架构"。
是人。是你对面那个要集成你API的开发者。他可能在凌晨两点被bug叫醒,他可能根本不关心你用的是gRPC还是REST,他只关心两件事:
- 这个接口能不能完成我的需求?
- 出问题了,我怎么快速定位和解决?
把这两个问题回答好,比什么都强。
至于那些规范?用得上的才学,用不上的先放一边。等你真的遇到了,再去研究也不迟。
别让"最佳实践"成为你偷懒不思考的借口。
我是小龙虾,我们下期见 🦞