RESTful API 为什么越来越被人嫌弃?我来说几句真话
做后端开发这些年,RESTful API 这四个字听过没有一万遍也有一千遍了。每个公司、每个团队、每个新人教程,开口就是「我们是 RESTful 设计」。但你要是让我说真话——大部分所谓「RESTful」 API,连 REST 的边都没沾上。
今天不客气地聊聊 RESTful 真实处境,以及什么才是好的 API 设计。
一、先说 REST 是什么,别把名字叫错了
REST 这个词是 Roy Fielding 2000 年在他的博士论文里提出来的。他是个很有个性的人,整篇论文把 HTTP/1.1 设计哲学翻了个底朝天。REST 全称是 Representational State Transfer,核心就一句话:用统一接口把资源状态在客户端和服务端之间传来传去。
六个约束:客户端-服务端、无状态、可缓存、分层系统、统一接口、超文本驱动。其中「统一接口」最核心,要求你用 URL 表示资源,用 HTTP 方法表示动作,用状态码表示结果。
但问题是——大部分人只知道「用 GET 查、用 POST 增、用 PUT 改、用 DELETE 删」,然后就觉得自己是 RESTful 了。真的就这么简单吗?
二、RESTful 真实的问题在哪里
1. 分页和过滤:URL 长得像灾难
假设你有个订单系统,要查询「状态为已支付、金额大于 1000」的订单。用 RESTful 的写法:
GET /orders?status=paid&amount_gt=1000
看起来还行?但当你需要嵌套资源查询,比如「获取用户 ID=123 的订单中,商品 ID=456 的那条订单详情」,URL 就变成了:
GET /users/123/orders?items_product_id=456&status=paid
这还算清醒的。要是加上分页、排序、字段筛选,URL 能写满两行。而且这里每个 query param 的语义其实全靠文档约定,没有标准,团队新人接手上个月写的接口,URL 读半天不知道啥意思。
2. 版本管理:业界根本没有标准答案
API 要升级怎么办?路径版本 /v1/?Header 版本?还是 Query 参数?三种写法各有各的烂:
- 路径版本:
/api/v1/users— 直观,但每次升级 URL 全变,SEO 友好但代价大 - Header 版本:
Accept: application/vnd.api+v2— 符合 HTTP 规范,但调试极其反人类,用 curl 测试要记一长串 - Query 版本:
/users?version=2— 最随意,等于没版本管理
你说哪个对?抱歉,业界没有共识。REST 论文里压根没提版本管理这回事。这不是 RESTful 的锅,但用 RESTful 的团队迟早要面对。
3. 复合业务逻辑:无能为力
「转账」这个操作,REST 的思路是:你先查余额够不够,再发起交易,再更新账户——三步。这在纯资源模型里是合理的。但实际上这种跨资源的原子操作,在 REST 里只能用 POST 加一个「交易」资源来模拟,写出来像这样:
POST /transactions{"from_account": "ACC001","to_account": "ACC002","amount": 1000}
这个设计其实挺好。但问题是当业务复杂起来,你分不清到底该用哪个资源来承载操作——「取消订单」是用 DELETE /orders/123 还是 POST /orders/123/cancel?后者已经有点违背 REST 的统一接口原则了,但不用的话,业务语义说不清。
三、GraphQL 和 RPC 为什么会崛起
2015 年 Facebook 开源 GraphQL,紧接着国内「面向接口编程」的思想开始退潮,取而代之的是「面向产品体验编程」。GraphQL 解决的核心问题是:客户端需要什么字段,服务端就返回什么字段,别废话。
这击中了 RESTful 的一个软肋:Over-fetching。你要一个用户的基本信息,REST 返回 50 个字段,其中 49 个你用不上。移动端讲究流量控制,这种浪费是致命的。
而 gRPC 代表的 RPC 流派呢?它的核心逻辑是:API 就是远程函数调用,我要像调用本地函数一样调用远程服务。ProtoBuf 二进制序列化,性能碾压 JSON,强类型接口定义,文档即代码。这些优势在微服务内部通信场景里,是 RESTful 完全比不了的。
所以现在的事实是:对外的、需要 SEO 和广泛生态的 Web API 用 REST(或者近似 REST);内部微服务通信用 gRPC;需要灵活查询的用 GraphQL。没有银弹,只有取舍。
四、我认为什么是好的 API 设计
基于这些年踩过的坑,我总结了几条实用的原则,不追求名词正确,只追求「好用、好维护、好沟通」:
1. URL 只描述资源,行为交给 HTTP 方法和 body
好的:POST /orders/123/cancel — 语义清晰,知道这是个取消操作
坏的:POST /cancelOrder?id=123 — 把动作放进了 URL,资源反而模糊了
2. 错误信息要像文档一样写
很多团队的错误返回是这样的:
{ "code": 10001, "message": "操作失败" }
10001 是谁定的?操作失败是什么操作?失败原因是啥?调用方拿到这个只能干瞪眼。
好的错误返回应该是:
{ "code": "INSUFFICIENT_BALANCE", "message": "账户余额不足,当前余额 850.00,需 1000.00", "field": "amount", "docs": "https://api.example.com/errors#INSUFFICIENT_BALANCE" }
有错误码、有人类可读的信息、有具体字段、有文档链接。调试的时候,一线开发不需要来回问后端「这个错误什么意思」。
3. 永远提供 dry run 的能力
对于写操作(创建、修改),如果 API 支持,建议提供一个「预演」模式:只校验参数,不实际执行。这样调用方可以在提交前知道会不会失败,避免产生垃圾数据。
POST /orders?dry_run=true{ "user_id": "U123", "items": [...] }
返回结果会告诉你校验是否通过,以及每个字段的校验状态。
4. 幂等性要显式声明
HTTP 规范说 GET、PUT、DELETE 是幂等的,POST 不是。但实际上很多 PUT 实现并不满足幂等(如果资源有 created_at 这种字段)。与其靠规范约定,不如在响应头里显式声明:
Idempotency-Key: <unique-request-id>X-Idempotent: true
调用方看到这个头,就知道可以放心重试,不需要自己实现重试去重逻辑。
五、写在最后
RESTful 不是银弹,GraphQL 和 gRPC 也不是。技术选型这种东西,最忌讳的就是「我觉得这个很潮」而不是「这个最适合我的场景」。
我在项目里见过太多「为了 RESTful 而 RESTful」的设计:明明是个高度交互式的游戏后端,偏要套 RESTful 的壳,结果接口写得比 RPC 还难懂;也见过明明是简单的数据 CRUD,偏要上 GraphQL,最后 Schema 管理成一团乱麻。
好的 API 设计只有一个标准:调用方用起来爽,维护者改起来不骂你。做到这两点,管你叫什么名字,都是好 API。
别被名词绑架了。