了解 Roy Fielding 的 REST 约束及其如何影响实际 API 与 Web 应用设计:客户端-服务器、无状态、缓存、统一接口、分层等。

Roy Fielding 不只是一个与 API 流行词绑在一起的名字。他是 HTTP 和 URI 规范的主要作者之一,在他的博士论文中提出了一种叫做 REST(Representational State Transfer) 的架构风格,用来解释 Web 为什么能如此有效地运行。
这个起源很重要,因为 REST 并不是为“创建好看端点”而发明的。它是用来描述那些让全球性、混杂网络仍能扩展的约束:大量客户端、大量服务器、中间层、缓存、部分失败以及持续变化。
如果你曾想知道为什么两个“REST API”感觉完全不同——或者为什么一个小的设计决定会变成分页的痛点、缓存混乱或破坏性改动——本指南旨在减少这些惊讶。
你会收获:
REST 不是检查清单、协议或认证。Fielding 将其描述为一种架构风格:一组约束,当它们一起应用时,会产生像 Web 那样可扩展的系统——易于使用、能随时间演进并对中间层(代理、缓存、网关)友好而无需持续协调。
早期的 Web 必须在许多组织、服务器、网络和客户端类型之间工作。它必须在没有中央控制的情况下增长,能在部分失败中生存,并允许新功能出现而不破坏已有功能。REST 通过偏好少量广泛共享的概念(如标识符、表示、标准操作)而不是自定义、紧耦合的契约来解决这些问题。
约束是限制设计自由的规则,以换取好处。例如,你可能放弃服务器端会话状态,这样任何服务器节点都可以处理请求,从而提高可靠性和扩展性。每个 REST 约束都做类似的权衡:牺牲一些任意灵活性,换来更多的可预测性和可演化性。
许多 HTTP API 借用了 REST 的一些想法(HTTP 上的 JSON、URL 端点、可能还有状态码),但并没有应用完整的约束集合。这并不是“错误”——这常常反映出产品交付期限或仅限内部使用的需求。区别在于:一个 API 可以是面向资源的,却并非完全符合 REST。
把 REST 系统想成资源(可以用 URL 命名的事物),客户端通过表示(资源的当前视图,例如 JSON 或 HTML)与之交互,并由链接(下一步动作和相关资源)引导。客户端不需要秘密的带外规则;它遵循标准语义并用链接导航,就像浏览器在 Web 上移动一样。
在陷入约束和 HTTP 细节之前,REST 从一个简单的词汇转变开始:以资源而不是动作来思考。
资源 是系统中一个可寻址的“事物”:用户、发票、产品类别、购物车。重要的是它是一个有身份的名词。
这就是为什么 /users/123 读起来很自然:它标识了 ID 为 123 的用户。与此相比,以操作为形的 URL 如 /getUser 或 /updateUserPassword 描述的是动词——操作——而不是你正在操作的事物。
REST 并不说你不能执行动作。它表示动作应通过统一接口来表达(对于 HTTP API,通常意味着 GET/POST/PUT/PATCH/DELETE 等方法)作用于资源标识符。
表示(representation) 是你在线上发送的资源在某一时刻的快照或视图。同一个资源可以有多种表示。
例如,资源 /users/123 可以为应用返回 JSON,也可以为浏览器返回 HTML。
GET /users/123
Accept: application/json
可能返回:
{
"id": 123,
"name": "Asha",
"email": "[email protected]"
}
而:
GET /users/123
Accept: text/html
可能返回渲染相同用户详情的 HTML 页面。
关键思想:资源不是 JSON,也不是 HTML。那些只是用来表示它的格式。
一旦你围绕资源和表示来建模 API,几个实践决策会变得更容易:
/users/123 即使你的 UI、工作流或数据模型演进仍然有效。这种以资源为先的思维是 REST 约束构建的基础。没有它,“REST”往往就沦为“HTTP 上的 JSON,加上一些漂亮的 URL 模式”。
客户端—服务器分离是 REST 用来强制关注点清晰划分的方式。客户端专注于用户体验(人们看到和做的事),而服务器专注于数据、规则和持久化(什么是真,什么是允许的)。当你把这些关注点分开时,每一方都可以改变而不用强迫另一方重写。
在日常意义上,客户端是“表现层”:界面、导航、快速反馈的表单校验和乐观 UI 行为(比如立即显示新评论)。服务器是“事实来源”:认证、授权、业务规则、数据存储、审计以及任何必须在设备间保持一致的东西。
一个实用规则:如果一个决定影响安全、金钱、权限或共享数据一致性,它就属于服务器。如果一个决定只影响体验感(布局、本地输入提示、加载态),就放在客户端。
这个约束直接映射到常见的架构:
客户端—服务器分离是让“一个后端,多前端”可行的关键。
一个常见错误是把 UI 工作流状态存储在服务器(例如:“用户在结账的第几步”)的会话中。这会把后端耦合到特定的屏幕流程并使扩展变得更难。
更好的做法是每次请求携带必要的上下文(或从持久化资源中推导),这样服务器专注于资源和规则——而不是记住某个特定 UI 的进度。
无状态意味着服务器不需要在请求之间记住关于客户端的任何信息。每个请求都携带理解它并正确响应所需的所有信息:调用者是谁、他们想要什么以及处理所需的任何上下文。
当请求相互独立时,你可以在负载均衡后面增加或移除服务器而不必担心“哪个服务器记住我的会话”。这提高了可扩展性和弹性:任何实例都可以处理任何请求。
它也简化了运维。调试通常更容易,因为完整的上下文在请求(和日志)中可见,而不是隐藏在服务器端会话内存中。
无状态的 API 通常每次调用会发送更多数据。客户端不依赖已存的服务器会话,而是每次包含凭证和上下文。
你还必须对“有状态”用户流程(如分页或多步骤结账)显式处理。REST 并不禁止多步骤体验——它只是把状态推到客户端或放到可标识并可检索的服务器端资源上。
Authorization: Bearer … 头,以便任意服务器可以验证它。Idempotency-Key,以便重试不会重复工作。X-Correlation-Id 这样的头可以让你在分布式系统中追踪一次用户操作及其日志。对于分页,避免“服务器记住第 3 页”。优先使用显式参数如 ?cursor=abc 或客户端可跟随的 next 链接,将导航状态保存在响应中而非服务器内存中。
缓存是关于在安全的前提下重用之前的响应,这样客户端(或沿途的中间件)就不用每次都向你的服务器请求同样的工作。做得好会降低用户感知延迟并减少你的负载——而不改变 API 的语义。
当另一个请求在一段时间内接收相同载荷是安全的时,该响应就是可缓存的。在 HTTP 中,你用缓存头来表达这一意图:
Cache-Control:主要的控制开关(保留多长时间、是否允许共享缓存等)ETag 和 Last-Modified:验证器,让客户端询问“这有没有改变?”并得到廉价的“未修改”答案Expires:较老的表达新鲜度的方式,仍然存在于现实中这不仅仅是“浏览器缓存”。代理、CDN、API 网关甚至移动应用在规则明确时都可以重用响应。
好的缓存候选对象:
通常不适合缓存的:
private 缓存规则)关键点:缓存不是事后想到的。它是一个 REST 约束,会奖励那些清晰传达新鲜度和验证方式的 API。
统一接口 常常被误解为“用 GET 读取、POST 创建”。那只是小部分。Fielding 的想法更大:API 应该足够一致,使得客户端不需要为每个端点持有特殊知识即可使用它们。
资源的标识:用稳定的标识符(通常是 URL)来命名事物,比如 /orders/123,而不是 /createOrder。
通过表示进行操作:客户端通过发送表示(JSON、HTML 等)来改变资源。服务器控制资源;客户端交换它的表示。
自描述消息:每个请求/响应都应携带足够的信息来说明如何处理——方法、状态码、头、媒体类型和清晰的主体。如果意义隐藏在带外文档里,客户端就会高度耦合。
超媒体(HATEOAS):响应应包含链接和允许的动作,以便客户端在不硬编码每个 URL 模式的情况下跟随工作流。
一致的接口让客户端不那么依赖服务器内部细节。随着时间推移,这意味着更少的破坏性更改、更少的“特殊情况”,以及当团队演进端点时更少的返工。
200 表示读取成功,201 表示资源已创建(并带 Location),400 表示校验问题,401/403 表示认证/授权,404 表示资源不存在。code、message、details、requestId。Content-Type、缓存头),这样消息可以自行解释。统一接口关乎可预测性和可演化性,而不仅仅是“正确的动词”。
“自描述”消息告诉接收方如何解释它——无需带外的部落知识。如果一个客户端(或中间件)仅凭 HTTP 头和主体就无法理解响应的含义,你就创造了一个挂在 HTTP 上的私有协议。
最简单的做法是明确 Content-Type(你发送的是什么)和常常使用 Accept(你想要什么)。Content-Type: application/json 告诉客户端基本的解析规则,但在语义重要时你可以使用供应商媒体类型或 profile 来更进一步。
一些做法示例:
application/json 加上维持良好的模式。对多数团队最简单。application/vnd.acme.invoice+json 来标识特定表示。application/json,添加 profile 参数或链接到定义语义的 profile。版本控制应保护现有客户端。常见选项包括:
/v1/orders):明显,但可能鼓励分叉表示而不是演进它们。Accept):保持 URL 稳定并把“这是什么意思”作为消息的一部分。不论选择哪种方式,默认追求向后兼容:不要随意重命名字段,不要悄然改变含义,把删除当作破坏性变更处理。
当错误在各处长得一样时,客户端学得更快。挑选一种错误形状(例如 code、message、details、traceId)并在所有端点中使用它。使用清晰、可预测的字段名(createdAt vs created_at),并坚持一种命名约定。
好的文档能加速使用,但它不能是意义存在的唯一地点。如果客户端必须阅读维基才能知道 status: 2 表示“已支付”还是“待处理”,那么消息就不是自描述的。良好设计的头、媒体类型和可读的载荷能减少对文档的依赖并让系统更易演进。
超媒体(通常简称 HATEOAS:Hypermedia As The Engine Of Application State)意味着客户端不必事先“知道” API 的下一个 URL。相反,每个响应包含可发现的下一步作为链接:去哪里、哪些动作可行、以及有时应该使用哪个 HTTP 方法。
客户端不再硬编码诸如 /orders/{id}/cancel 的路径,而是跟随服务器提供的链接。服务器实质上在说:"根据当前资源状态,这些是有效的动作。"
{
"id": "ord_123",
"status": "pending",
"total": 49.90,
"_links": {
"self": { "href": "/orders/ord_123" },
"payment":{ "href": "/orders/ord_123/payment", "method": "POST" },
"cancel": { "href": "/orders/ord_123", "method": "DELETE" }
}
}
如果订单后来变为 paid,服务器可能不再包含 cancel,而是新增 refund——只要做好遵循链接的客户端不会被破坏。
当流程会演进时超媒体最为出色:入职步骤、结账、审批、订阅,或任何“接下来允许做什么”会根据状态、权限或业务规则变化的过程。
它也能减少硬编码 URL和脆弱的客户端假设。你可以重组路由、引入新动作或弃用旧动作,同时只要保持链接关系的语义,客户端就能正常工作。
团队通常跳过 HATEOAS,因为它看起来像额外工作:定义链接格式、约定关系名,并教客户端开发者跟随链接而不是构造 URL。
失去的是 REST 的一个关键好处:松耦合。没有超媒体,很多 API 就变成了“基于 HTTP 的 RPC”——它们可能使用 HTTP,但客户端仍然严重依赖带外文档和固定 URL 模板。
分层系统意味着客户端不必知道(且通常无法看出)它是在与“真实”的源服务器通信还是与沿途的中间层交互。这些层可以包括 API 网关、反向代理、CDN、鉴权服务、WAF、服务网格,甚至微服务之间的内部路由。
分层创建了清晰的边界。安全团队可以在边缘强制 TLS、速率限制、认证和请求校验而无需改动每一个后端服务。运维团队可以在网关后水平扩展、在 CDN 添加缓存或在事故期间切换流量。对客户端而言,这简化了事情:一个稳定的 API 端点、一致的头和可预测的错误格式。
中间层可能引入隐藏延迟(额外跳数、额外握手)并使调试更难:错误可能出在网关规则、CDN 缓存或源代码上。当不同层以不同方式缓存或网关重写了影响缓存键的头时,缓存也可能变得令人困惑。
当系统可观测且可预测时,分层是一个优势。
按需代码是明确可选的 REST 约束。它意味着服务器可以通过发送可执行代码来扩展客户端的能力。客户端无需预先把所有行为打包,而可以按需下载新的逻辑在本地执行。
如果你曾加载一个随后变得可交互的网页——表单校验、绘图、表格过滤——你已经使用过按需代码。服务器交付 HTML 和数据,同时交付运行在浏览器上的 JavaScript 来提供行为。
这是 Web 能迅速演进的一个重要原因:浏览器可以保持为一个通用客户端,而站点可以交付新功能而无需用户安装完整的新应用。
REST 在没有按需代码的情况下仍然“奏效”,因为其它约束已经能提供可扩展性、简单性和互操作性。一个 API 可以纯粹面向资源——提供像 JSON 这样的表示——而客户端实现各自的行为。
事实上,许多现代 Web API 故意避免发送可执行代码,因为这会带来复杂性:
当你控制客户端环境并需要快速推出 UI 行为,或想要一个薄客户端从服务器下载“插件”或规则时,按需代码可能有用。但它应被视为一个额外工具,而不是必须。
关键结论:你可以在不使用按需代码的情况下完全遵循 REST——许多生产环境的 API 都是这样,因为该约束只是关于可选的扩展能力,而非资源交互的基础。
大多数团队并不完全拒绝 REST——他们采用了一种“类 REST”风格,用 HTTP 作为传输同时悄悄放弃关键约束。这可以没问题,只要那是经过深思熟虑的权衡,而不是后来变成脆弱客户端和昂贵重构的意外。
反复出现的一些模式:
/doThing、/runReport、/users/activate——命名简单、接线方便。/createOrder、/updateProfile、/deleteItem——HTTP 方法变成事后的想法。这些选择在早期看起来高产,因为它们映射了内部函数名和业务操作。
用它来审查“我们到底有多 REST”:
/orders/{id} 而非 /createOrder。Cache-Control、ETag 和 Vary。REST 约束不是理论——它们是你在交付时会感受到的护栏。当你快速生成一个 API(例如为 React 前端搭建一个 Go + PostgreSQL 后端时),最容易犯的错误就是让“最快上手”的方式决定你的接口。
如果你在像 Koder.ai 这样基于对话构建 Web 应用的平台上工作,将这些 REST 约束早早带入讨论会有帮助——先命名资源、保持无状态、定义一致的错误形状并决定哪些地方可以安全缓存。这样,即使快速迭代,也能产出对客户端可预测且更易演进的 API。(并且因为 Koder.ai 支持源代码导出,你可以在需求演进时继续打磨 API 合约和实现。)
先定义关键资源,然后有意识地选择约束:如果你跳过缓存或超媒体,就记录原因和替代方案。目标不是纯粹主义——而是清晰:稳定的资源标识、可预测的语义和明确的权衡,使客户端在系统演化时保持弹性。
REST(Representational State Transfer)是 Roy Fielding 在他的论文中提出的一种架构风格,用来解释 Web 能够扩展的原因。
它不是一种协议或认证——而是一组约束(客户端-服务器、无状态、可缓存、统一接口、分层系统,可选的按需代码),这些约束用灵活性换取可扩展性、可进化性和互操作性。
因为很多 API 只采用了 REST 的部分思想(比如用 JSON over HTTP、好看的 URL),却跳过了其它关键点(如缓存策略或超媒体)。
两个“REST API”之间可能差别很大,取决于它们是否:
资源是一个你可以标识的名词(例如 /users/123)。动作端点则是在 URL 中把动词写死(例如 /getUser、/updatePassword)。
面向资源的设计通常更耐久,因为标识符保持稳定,而工作流和 UI 会变化。动作仍然可以存在,但通常通过 HTTP 方法和表示(representation)来表达,而不是把动词放到路径里。
资源是概念(“用户 123”)。表示(representation)是你传输的快照(JSON、HTML 等)。
这很重要,因为你可以在不改资源标识的情况下演进或新增表示。客户端应依赖资源的语义,而不是某一种具体的载荷格式。
客户端-服务器分离保持关注点独立:
如果某个决定影响到安全、资金、权限或共享一致性,就应该放在服务器端。这个分离使得“一个后端,多种前端”(Web、移动、合作方)成为可能。
无状态意味着服务器不依赖于跨请求记住客户端信息。每次请求都携带处理所需的全部信息(谁在调用、他们想要什么、以及处理所需的上下文)。
好处包括更容易水平扩展(任意节点都能处理请求)和更简单的调试(上下文在请求和日志中可见)。
常见实践:
Authorization: Bearer …?cursor=...)或响应中的 next 链接,而不是“服务器记住第几页”可缓存响应允许客户端或中间件在一段时间内安全地重用之前的响应,从而减少延迟并降低负载。
主要 HTTP 工具:
Cache-Control:控制新鲜度和范围ETag / Last-Modified:作为验证器以支持 304 Not Modified统一接口不仅仅是“正确使用 GET/POST/PUT/DELETE”。它更强调一致性,使客户端不必为每个端点学特殊规则。
实践要点:
超媒体(HATEOAS)意味着响应包含可执行的下一步链接,客户端通过跟随这些链接而不是硬编码 URL 模板来继续状态机。
它在以下场景最有价值:当流程会根据状态或权限变化(如结账、审批、入职流程)时;服务端可以通过改变返回的链接来增加或移除可用动作,而不破坏遵循链接的客户端。
团队通常跳过它,因为需要额外设计(链接格式、关系名),代价是客户端更加依赖文档和固定路由。
分层系统允许中间件(CDN、网关、代理、鉴权层)存在,客户端不必知道究竟是哪一层响应了请求。
为了避免调试困难:
500)当系统保持可观测和可预测时,分层是一项强大的能力。
Vary:当响应基于请求头(例如 Accept)变化时使用经验法则:对公共、对所有用户相同的数据(产品目录、文档、参考数据)积极缓存;对用户特有的数据要谨慎(通常标为 private 或不可缓存)。
200、201 + Location、400、401/403、404)code、message、details、requestId)这些做法能减少耦合,降低改变时破坏客户端的风险。