RESTful API 的“安魂曲”:我们都在做 RESTful RPC,别自欺欺人了

🔑 关键词:RESTful,HATEOAS,GraphQL,Richardson成熟度模型,API设计

📖 摘要:很多号称RESTful的API实则停留在RPC层级,本文结合个人经历、Richardson成熟度模型和GraphQL/gRPC对比,给出真实的API设计取舍。

我刚接手一个“全面RESTful化”的订单系统时,第一眼看到服务端代码,心跳漏了半拍。 POST /order/cancel,POST /order/pay,POST /order/send_goods。所有操作都以动词结尾,一个资源一个动作,像极了以前写RPC时的Service方法。 问架构师,他说‘我们已经用HTTP了呀,也分了资源,你看order、user、product都是独立URL,怎么不是REST?’ 我沉默了一会儿,从抽屉里翻出Roy Fielding的博士论文,掸了掸灰。 这句话我憋了很久才开口:‘如果只用POST加动词就算REST,那RPC只需要加个HTTP头也能叫REST了。’

图片

后来我逐渐习惯了,因为在很多团队里,RESTful从来不是一种架构风格,而是一种对外的营销话术。 老板要求‘接口要RESTful’,于是大家把/Services/DeleteUser改成DELETE /users/123,就以为完成KPI了。 但实际上,这些项目的问题没有消失:客户端依旧需要读文档才能知道下一步该调哪个接口;服务端依旧要为此写一堆action控制器;测试依旧为各种‘符合业务语义的异常状态码’写分支。 而这恰恰是REST理论上最应该解决的——如果按Fielding的约束,客户端可以从超媒体中无预知地发现后续动作,那整个合作模型就完全不同了。 所以,如果问我第一原则是什么,我会说:请在做一个所谓REST接口前,先问自己一句——客户端不读文档,能不能知道接下来要干什么?

图片

关于REST的深度,必须提Richardson成熟度模型。 Level 0是只用一个URI把HTTP当隧道,所有操作都是POST;Level 1才把资源拆开,/orders/123和/users/456;Level 2引入HTTP动词和状态码,成功返回200、创建返回201、无权限返回403;Level 3则要求每个响应里都带上可供下一步操作的链接,也就是HATEOAS。 我见过绝大多数团队把Level 2当成终点,然后宣称自己RESTful。 可Fielding本人早就说过,不满足超媒体约束的API不是RESTful,应该叫HTTP API。 这句话不是吹毛求疵,它是逻辑推演的结果:一旦没有超媒体,客户端就跟某一份私有文档挂钩,服务端任何演化都要通知所有调用方,这跟老式SOAP和RPC有一个病根——耦合。

图片

再看这十年的新对手。GraphQL把多端数据裁剪变成锦上添花,一个Query挑字段,但代价是服务端每个字段的resolver都要独立设计,缓存和权限都得下沉到字段级,很多团队用了一阵子后会发现,API变重了。 gRPC用HTTP/2做内部调用爽快,protobuf强类型、支持双向流,可对浏览器以及跨组织协作来说,没法直接用,只能靠grpc-web网关再转换成REST。 所以这不是谁取代谁的问题,而是要知道各自的舒适区在哪。 对外、长周期、要容忍第三方客户端接入的系统,REST的Level 2已经足够稳定;微服务之间通信直接上gRPC能省掉各种手写客户端;如果前端频繁为了字段配比喊救命而你们又有精力去扛Schema复杂度,GraphQL可以一试。 独立观点:我会把REST的“资源化”当作第一步,把“超媒体”当作理想,但绝不会把它当道德绑架工具去羞辱工程团队。

图片

最后分享一点很具体的建议:别迷信URL里不能有动词,登录/登出这种状态切换,把它设计成token资源的创建和删除更REST,但如果保持/login,你的团队和下游都明白,它也比把系统拆成十几个别扭的资源强。 用PATCH做部分更新,千万别把所有变更都堆到PUT里;错误响应请遵循RFC 7807,用application/problem+json返回统一结构,而不是每个team随心所欲拼一个{code,msg};更重要的是,别用HTTP 200包业务错误——在很多公司代码review里,这已经是红线级过错。 这些原则不一定让你升任架构师,但能减少你们在凌晨三点处理接口兼容问题的次数。 至少对我而言,API设计不是宗教,是工程取舍;RESTful这个单词承载过理想,但我不希望它成为皇帝的新衣。

图片

🏷️ 标签: