一次 API 评审里,有人问:"为什么这些接口全是 POST,不能改成 RESTful API 吗?"
这个问题很常见,却把三件事混在了一起:HTTP 方法、URL 命名和接口的设计思想。结果往往是团队花了一下午争论 URL 里能不能出现动词,最后只是把 /createOrder 改成 /orders,系统的耦合方式却一点没变。
要真正分清 RPC 和 REST,先记住一句话:
RPC 围绕"动作"组织接口;REST 围绕"资源"组织接口,并用一套统一语义操作这些资源。
这不是文字游戏。选择动作还是资源,会影响调用方需要知道多少业务流程,也会影响重试、缓存、状态恢复和接口演进。
先用同一个需求看懂区别
假设我们要提供"生成报告"的远程能力。生成过程可能持续几分钟,调用方还要查询进度和取消任务。
RPC 风格会把它设计成几个远程方法:
text
startReportExport(params)
getReportExportStatus(exportId)
cancelReportExport(exportId)
如果使用 HTTP 承载,接口可能是:
http
POST /report-service/start-export
POST /report-service/get-export-status
POST /report-service/cancel-export
调用方看到的是一组能力:开始、查询、取消。服务端每增加一种业务动作,通常就增加一个方法。
REST 风格会先问另一个问题:生成报告以后,系统里出现了什么值得被识别和管理的东西?
答案是"一次导出任务"。于是它被建模为资源:
http
POST /exports
GET /exports/export-123
DELETE /exports/export-123
第一次请求创建导出任务,服务端可以返回:
http
HTTP/1.1 201 Created
Location: /exports/export-123
json
{
"id": "export-123",
"status": "running",
"progress": 20
}
之后,调用方读取或终止的是同一个资源。
两种设计最后可能调用完全相同的业务代码。区别不在服务端"做了什么",而在它给调用方展示了什么模型:
| RPC | REST | |
|---|---|---|
| 调用方首先看到 | 可以执行的方法 | 可以识别的资源 |
| 接口的主要词汇 | start、cancel、publish |
URI、资源表示、HTTP 方法 |
| 新能力通常如何出现 | 增加方法 | 增加资源、关系或表示 |
| 调用方如何了解状态 | 调用专用查询方法 | 获取资源当前表示 |
最简洁的判断方法是:
如果接口主要在回答"服务能替我做什么",它更接近 RPC;如果接口主要在回答"系统中有哪些对象,它们现在是什么状态",它更接近 REST。
RPC 是什么:把网络藏在函数调用后面
RPC 是 Remote Procedure Call,也就是远程过程调用。
它希望让调用远端服务尽量接近调用本地函数:
ts
const result = await paymentService.transfer({
from: 'account-a',
to: 'account-b',
amount: 100,
})
真实执行会经过序列化、网络传输、鉴权、超时和服务端处理,但调用方主要面对的是:
- 服务名;
- 方法名;
- 输入参数;
- 返回值;
- 方法级错误。
gRPC 是 RPC 的一种具体实现。它通过接口定义描述服务和方法,再生成客户端与服务端代码。JSON-RPC、Dubbo,以及大量 POST /service/doSomething 的 HTTP 接口,也都采用了相似的动作模型。
RPC 的优势很直接:业务意图清楚、类型契约集中、代码生成方便,也适合流式调用和内部高频通信。
它的代价是调用方容易依赖服务端的操作流程。例如调用方必须知道:先 prepare,再 commit,最后 activate。如果服务端以后想合并步骤或调整顺序,客户端也可能需要变化。
REST 是什么:用统一接口操作资源
REST 是 Representational State Transfer。它不是某个框架,也不是一种报文格式,而是一组面向分布式系统的架构约束。
REST 包含客户端与服务端分离、请求无隐藏会话依赖、缓存、分层系统和统一接口等约束。这里最关键的是"统一接口"。
RPC 可以为每种业务定义专用方法:
text
approveOrder
cancelOrder
refundOrder
archiveOrder
REST 则刻意收敛操作词汇。不同资源尽量共享 GET、POST、PUT、DELETE 等方法语义。资源之间的差异主要通过 URI、表示和链接表达。
这样做并不是为了让 URL 好看,而是让浏览器、网关、代理、缓存和监控系统在不了解具体业务代码的情况下,仍能理解请求的基本意图。
例如,GET 应该表示读取资源,而不是删除数据;PUT 表示用请求中的表示创建或替换目标资源状态;DELETE 表示移除目标 URI 与当前功能之间的关联。中间系统因此可以对不同服务复用相同处理规则。
REST 为这种通用性付出了代价:统一接口不一定是某个具体业务最高效、最直接的表达。计算型操作、实时流和高度专用的内部调用,往往用 RPC 更自然。
REST 的资源不是数据库表
"资源"是理解 REST 最容易卡住的地方。
资源不是数据库表,也不要求与某一行数据一一对应。它是一个可以被稳定命名和引用的概念。
下面这些都可以是资源:
- 一个用户;
- 一份文档;
- 某个订单的当前状态;
- 一次异步导出任务;
- 某份文件的指定版本;
- 一次审批申请;
- 一组搜索结果。
判断一个概念是否值得成为资源,可以问:
- 它是否需要稳定身份?
- 创建后是否还要查询?
- 是否会被其他对象引用?
- 是否有创建、运行、完成、取消或过期等生命周期?
- 失败后是否需要恢复或审计?
如果多数答案是"是",把它藏在一个动作接口后面,调用方通常会失去观察和治理它的能力。
例如下面这个接口名叫预览:
http
POST /imports/preview
如果它真的只计算并返回结果,动作式设计没有明显问题。
但如果它还会写数据库、预分配 ID,并永久占用一个幂等键,那么它实际上创建了持久状态。更诚实的模型可能是:
http
POST /import-preparations
GET /import-preparations/preparation-123
DELETE /import-preparations/preparation-123
这时需要讨论的不再是"为什么用了 POST",而是:准备记录能不能查询、取消、过期和恢复。
为什么只看 URL 会判断错
下面这个接口使用名词,但本质仍然可能是 RPC:
http
POST /operations
{
"method": "cancelOrder",
"params": {
"orderId": "123"
}
}
因为调用方仍然是在选择远程方法。
反过来,URL 里出现业务含义,也不代表它一定违背 REST:
http
POST /orders/123/refunds
这可以解释为在订单的退款集合中创建一笔退款资源。退款有自己的身份、金额、状态和审计记录,是一个真实资源,而不是为了躲避动词生造出来的名词。
因此不要用下面这些口诀机械判断:
- 有动词就是 RPC;
- 全是名词就是 REST;
- 使用
POST就不 REST; - 做完 CRUD 就是 REST。
更可靠的问题是:接口是否让重要状态获得稳定身份,并且 HTTP 方法是否保持一致、可预期的语义。
HTTP、RPC 和 REST 到底是什么关系
HTTP、RPC 和 REST 可以放在不同层次理解:
text
业务模型
动作模型 RPC
资源模型 REST
应用协议
HTTP
gRPC protocol
数据格式
JSON
Protobuf
因此:
- RPC 可以通过 HTTP 传输;
- REST API 通常使用 HTTP,但 REST 不等于 HTTP;
- 使用 JSON 与采用哪种风格无关;
- 一个系统可以对外提供资源 API,对内使用 gRPC;
- 同一个 HTTP API 也可以同时包含资源接口和命令接口。
HTTP 规范本身甚至指出,请求方法有点像对目标对象发起远程方法调用。两者真正的分界是:HTTP 的标准方法不是每个业务对象私有的方法,GET 或 PUT 应该在不同资源上保持相同的基本语义。
换句话说,RPC 的方法词典通常跟着业务增长;REST 的方法词典相对稳定,增长的是资源和资源关系。
两种抽象会带来哪些实际差异
1. 调用方依赖什么
RPC 调用方通常依赖"正确的动作顺序":
text
create
publish
activate
REST 调用方通常依赖"资源当前状态以及允许的状态变化":
text
draft
published
active
RPC 更容易暴露流程;REST 更容易暴露结果和状态。但如果 REST 客户端仍然硬编码全部步骤,它同样会与服务端状态机紧密耦合。
2. 状态能否被重新发现
远程调用随时可能遇到这种情况:服务端已经执行成功,但响应在网络中丢失。
RPC 需要专门设计查询或重放方法:
text
getOperationResult(requestId)
如果操作本身已经成为资源,REST 可以直接读取:
http
GET /exports/export-123
这不是说 REST 自动解决了分布式失败,而是资源模型通常会迫使设计者给重要状态一个可重新查询的地址。
3. 幂等和重试如何表达
HTTP 中,"安全"和"幂等"是两件事:
- 安全表示调用方没有请求改变业务状态,例如
GET; - 幂等表示相同请求执行多次,其预期最终效果与执行一次相同,例如符合语义的
PUT和DELETE。
幂等方法便于客户端在响应丢失后自动重试。但方法名称只是契约,不会替应用实现数据库唯一约束、并发控制和结果重放。
同样,POST 默认不承诺幂等,却可以通过幂等键和请求摘要实现业务幂等;RPC 方法也可以是幂等的。
所以幂等不是 REST 独有能力。REST 的价值是把一部分通用语义公开给客户端和中间系统。
4. 缓存和通用基础设施能否参与
一个稳定、可读取的资源:
http
GET /exports/export-123
可以自然使用缓存控制、ETag、条件请求和标准监控。
下面这种接口虽然也能返回状态,但通用缓存通常很难理解它:
http
POST /report-service/get-export-status
RPC 不是不能缓存,而是需要调用双方或框架额外约定;REST 希望更多组件只理解通用协议就能参与。
5. 接口如何演进
RPC 通常以方法和消息结构为演进单位:增加方法、增加字段、保留旧方法或发布新版本。
REST 通常以资源表示和关系为演进单位:增加可选字段、增加子资源、增加新的链接关系,同时保持既有 URI 和方法语义。
二者都可能设计出兼容性很差的 API。REST 不会自动消除版本问题,RPC 也不必然意味着紧耦合。真正关键的是,对外暴露的概念是否稳定。
不要追求纯 REST 而制造假资源
把任何动作都改写成名词,并不会自动改善设计。
例如一次无状态计算:
text
calculateTax(input)
compileSource(input)
validateExpression(input)
调用方关心的是执行能力和结果,没有后续需要查询、引用或治理的对象。RPC 往往更直接。
相反,下面这些概念更适合资源化:
- 要运行几分钟的任务;
- 可以暂停、取消或重试的操作;
- 需要审计的审批和退款;
- 会被其他对象引用的不可变版本;
- 失败后需要人工恢复的导入过程。
资源化的目的不是消灭动词,而是不要把持久状态藏起来。
实际项目通常应该混合使用
一个现实系统完全可以这样组合:
http
GET /orders/123
POST /orders/123/refunds
GET /refunds/refund-456
订单和退款是资源,创建退款仍然使用 POST。
服务内部则可以通过 RPC 调用支付系统:
text
PaymentService.refund(paymentId, amount)
两者没有冲突:
- 外部 API 用资源模型提供稳定身份、查询和生命周期;
- 内部 RPC 用动作模型提供明确、高效的服务调用;
- 事件系统再负责传播"退款已完成"这样的事实。
比"全系统必须统一成一种风格"更重要的是:每一层选择的抽象是否适合它要解决的问题。
一套真正可用的选择方法
设计接口时,可以按顺序问四个问题。
问题一:调用结果是否需要长期身份
如果结果需要被查询、引用、审计或恢复,优先考虑资源。
如果只是即时计算,优先考虑 RPC。
问题二:调用方关心过程还是状态
如果调用方天然在说"执行校验""计算价格""发送指令",动作模型更直接。
如果调用方更关心"任务现在运行到哪里""订单现在是什么状态",资源模型更清晰。
问题三:通用 HTTP 设施有没有价值
如果需要浏览器兼容、缓存、条件请求、网关治理和第三方客户端,资源化收益更大。
如果是组织内部、强类型、低延迟或流式服务调用,RPC 往往更合适。
问题四:失败后如何找回结果
如果响应丢失后只能重复执行一个动作,却无法查询原操作是否成功,说明接口缺少身份或恢复机制。
解决办法不一定是 REST,但必须补上请求身份、幂等、状态查询或结果重放。
最后可以把选择压缩成一张表:
| 更倾向 RPC | 更倾向 REST |
|---|---|
| 即时计算 | 长生命周期对象 |
| 强类型内部服务 | 开放或跨团队 API |
| 流式通信 | 可缓存读取 |
| 调用方关心执行动作 | 调用方关心资源状态 |
| 方法语义比资源身份稳定 | 资源身份比内部流程稳定 |
最后再回到最初的问题
看到一组全部使用 POST 的接口时,不要立刻问"为什么不用 REST"。先问:
- 这些接口是在暴露动作,还是在管理有身份的状态?
- 某个动作执行后,是否产生了需要查询、取消、过期或恢复的对象?
- HTTP 方法表达的语义,是否和真实副作用一致?
- 响应丢失后,调用方能否安全重试或找回结果?
如果一个所谓的预览接口其实写入了长期状态,问题不是它用了 POST,而是它隐藏了自己的生命周期。
如果一个操作只是无状态计算,强行给它制造资源也不会让设计更高级。
RPC 与 REST 不是新旧技术之争,也不是动词和名词之争。它们是在回答两个不同的问题:
RPC 先问"远端能做什么";REST 先问"远端有哪些可识别的资源"。
先选对抽象,再讨论 URL 和 HTTP 方法,API 设计才不会停留在表面。
参考与数据源
- Roy Thomas Fielding, Architectural Styles and the Design of Network-based Software Architectures ,Chapter 5:REST 的约束、统一接口、资源与表示的原始定义。roy.gbiv.com/pubs/disser...
- RFC 9110, HTTP Semantics :HTTP 统一资源接口与请求方法概览。www.rfc-editor.org/rfc/rfc9110...
- RFC 9110, Safe Methods :安全方法的定义。www.rfc-editor.org/rfc/rfc9110...
- RFC 9110, Idempotent Methods :幂等方法和自动重试语义。www.rfc-editor.org/rfc/rfc9110...
- RFC 9110, POST :POST 的资源特定处理、资源创建与
Location语义。www.rfc-editor.org/rfc/rfc9110... - RFC 9110, PUT :PUT 的资源状态替换语义,以及它与 POST 的区别。www.rfc-editor.org/rfc/rfc9110...
- gRPC Documentation, Core concepts, architecture and lifecycle :RPC 服务、方法、参数、返回类型与代码生成模型。grpc.io/docs/what-i...