什么是RESTful API
摘要
本文解释了RESTful API的真正含义,强调了HATEOAS和超媒体作为应用状态引擎的重要性,并指出大多数现代API仅达到Richardson成熟度模型的第2级。
暂无内容
查看缓存全文
缓存时间: 2026/07/30 13:49
# API 达到 RESTful 意味着什么 | Andros Fenollosa
来源: https://en.andros.dev/blog/9761fd2e/what-it-means-for-an-api-to-be-restful/
很抱歉,但你其实并不真正了解什么是 RESTful API。我知道你是一位经验丰富的开发者,集成过各种类型的 API,甚至可能已经用多种语言在生产环境中部署过几十个 API。但这只能证明你遵循了行业最佳实践,阅读了海量文档——这很好。然而,这些都不能证明你真正掌握了 RESTful。别慌!你不是一个人。我认识的大多数开发者同样无法定义它、实现它,或是指出它的优势。因此,在这篇文章中,我将用易于理解的例子来解释它的优点。读完本文后,你很可能再也不会用同样的眼光看待 API 了。你已经被提醒过了!
API RESTful 常被用作 REST API(表述性状态转移)的同义词,而其中的细微差别很重要。REST 并非 HTTP 接口:它是一种架构风格,由 Roy Fielding 在其博士论文(2000 年)中定义,并施加了以下约束:
- **客户端-服务器架构**:客户端和服务器必须分离。
- **无状态**:每个客户端请求必须包含服务器理解和处理该请求所需的全部信息。换句话说,服务器不会在请求之间保留客户端的状态信息。
- **可缓存**:响应必须明确标记为可缓存或不可缓存。
- **分层系统**:架构可由多个层次组成,每层有特定功能且相互隔离。
- **统一接口**:客户端与服务器之间的通信必须可预测,并具有明确定义的模式。
- **按需代码(可选)**:服务器可以向客户端发送可执行代码(如 JavaScript 脚本),以扩展客户端的功能。
换句话说,REST 是一套用于提供资源的架构原则,通常基于 HTTP。它在 Web 开发中如此根深蒂固、如此标准化,以至于当我们看不到它时反而会觉得奇怪。
接下来是令人不安的部分:严格来说,REST 已经包含了你在本文中将要看到的全部内容。超媒体是统一接口约束的一部分,而 "RESTful" 只是形容词,意为 "符合 REST"。实际情况是,流行用法逐渐将 "REST" 降级为 "返回 JSON 的 HTTP API",以至于 Fielding 本人在 2008 年发表了一篇著名文章《REST APIs must be hypertext-driven》(https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven),抱怨我们将任何披着 HTTP 外衣的 RPC 都称为 REST。而《Richardson 成熟度模型》(https://martinfowler.com/articles/richardsonMaturityModel.html) 则用数字量化了这一差距:第 0 级是单个 RPC 风格的端点;第 1 级是资源拥有自己的 URI;第 2 级是正确使用 HTTP 动词和状态码;第 3 级是超媒体。你每天消费的绝大多数 "REST" API 都停留在第 2 级。
在本文中,我将用 RESTful 指代第 3 级,也就是 Fielding 要求的那一级。目标是将 API 转变为可预测、标准化且自描述的接口。客户端只需基础 URL,就能探索所有资源,无需依赖外部文档。此外,我们还能灵活地更改路由而不影响客户端,甚至可以使用多种通信协议。
但首先,你必须了解一个概念:**HATEOAS**(超媒体作为应用状态引擎)。这个概念对于理解 RESTful API 如何实现自描述和可导航至关重要。
## HATEOAS(超媒体作为应用状态引擎)
超媒体是 RESTful 的关键特征之一:它允许客户端通过响应中提供的链接动态发现资源。这些链接通常位于 `_links` 父键下。
```json
{
"id": 123,
"name": "John Doe",
"_links": {
"self": {
"href": "/users/123",
"method": "GET"
},
"update": {
"href": "/users/123",
"method": "PUT"
},
"delete": {
"href": "/users/123",
"method": "DELETE"
},
"friends": {
"href": "/users/123/friends",
"method": "GET"
},
"posts": {
"href": "/users/123/posts",
"method": "GET"
},
"search": {
"href": "/search/?query={query}",
"method": "GET",
"templated": true
}
}
}
```
借助这一点,客户端可以通过解析并跟随链接来导航 API,获取所需信息,这与你浏览网页的方式类似。此外,由于你是在相对路由(`href`)之间跳转,并使用链接名称(对象键)作为标识符,因此后端可以更改地址而不会影响客户端。这非常强大,因为客户端不绑定于固定的路由结构,而是在节点之间移动。
注意最后一个链接中使用了 `templated: true`。它表示 `href` 包含一个 URI 模板(遵循 RFC 6570),在使用前必须用具体值填充。这是一种发现参数化端点的优雅方式。例如,客户端可以将 `{query}` 替换为 "restful api" 以搜索相关内容。
在继续之前,诚实地说明一点:`_links` 约定来自 [HAL](https://stateless.co/hal_specification.html)(超文本应用语言),但 HAL 并未定义 `method` 字段;它的链接只考虑诸如 `href`、`templated`、`type` 或 `title` 等属性。我添加它是因为在实际中它有助于 API 自描述,并且是一种常见的扩展。如果你需要具有形式化方法和字段的动作,请查看 [Siren](https://github.com/kevinswiber/siren);如果你更喜欢纯 HAL,则省略 `method` 并信任协议的约定(规则 2)。
并非每个资源都需要超媒体,只有那些在正确上下文中相关的资源才需要。例如,在一篇博客文章中包含购物车的链接没有意义,但文章包含作者、评论或相关文章的链接则是有意义的。
现在我们已经理解了 HATEOAS 的重要性,接下来看看 RESTful API 必须遵循的规则。
## RESTful API 的 6 条规则
这些规则并非我凭空捏造:它们就是 Fielding 在上文提到的文章中列出的六个条件,此处进行了改编并附有示例。
### 1. 不要依赖单一协议
使用资源标识符(URI)来定义资源。换句话说,不要使用 URL(例如 `http://example.com/api/users/123`),而要使用忽略协议的 URI(例如 `/users/123`)。这样你就可以使用其他协议,如 WebSocket、MQTT、NNTP、RPC 等。当然你可以使用 HTTP,但你不能只依赖它。例如,如果你的 API 仅设计为通过 HTTP 工作,那么它严格来说就不是 RESTful 的。
### 2. 不要改变协议
不要重新发明轮子。不要在协议上发挥创意。例如,如果你使用 HTTP,就遵循约定:GET 用于获取资源,POST 用于创建,PUT 用于替换,PATCH 用于部分更新,DELETE 用于删除。使用正确的 HTTP 响应状态码:200 OK、201 Created、204 No Content、400 Bad Request、404 Not Found 等。使用 MQTT?请使用正确的命令和状态。
### 3. 关注媒体类型,而不是 URI
不要为每个 URI 编写外部文档,而应让你的 API 描述它所处理的媒体类型。这是六条规则中最容易被误解的一条,而 Fielding 认为几乎所有的描述性工作都应投入于此。思路是:不要发布路由列表(即我们通常所说的“文档”),而是定义并记录你的媒体类型(例如 `application/vnd.myshop.product+json`),即每个表示的字段含义以及如何处理其链接。客户端根据接收到的 `Content-Type` 来决定做什么,而不是根据它调用的 URI。URI 变成了可互换的细节。
例如,用户资源信息较少时可以是:
```json
{
"id": 123,
"name": "John Doe",
"_links": {
"self": "/users/123",
"friends": "/users/123/friends",
"lastInvoice": "/users/123/invoice/last"
}
}
```
而用户资源信息较多时可以是:
```json
{
"id": 123,
"name": "John Doe",
"_links": {
"self": {
"href": "/users/123",
"method": "GET",
"type": "application/json"
},
"friends": {
"href": "/users/123/friends",
"method": "GET",
"type": "text/csv"
},
"lastInvoice": {
"href": "/users/123/invoice/last",
"method": "GET",
"type": "application/pdf"
}
}
}
```
### 4. 不要假设 URI 结构
你不能在客户端存储或重用 URI 结构。API 可能会在没有通知的情况下更改它们,而你的客户端显然会停止工作。相反,应该解析链接并跟随相对标识符(`rel`)。
一个资源可以具有以下结构:
```json
{
"id": 987,
"name": "Totoro plush",
"price": 19.99,
"_links": {
"self": {
"href": "/products/987",
"method": "GET"
},
"addToCart": {
"href": "/cart/add/987",
"method": "POST"
},
"reviews": {
"href": "/products/987/reviews",
"method": "GET"
}
}
}
```
而第二天它可能变成:
```json
{
"id": 987,
"name": "Totoro plush",
"price": 19.99,
"_links": {
"self": {
"href": "/shop/item/987",
"method": "GET"
},
"addToCart": {
"href": "/shop/cart/987",
"method": "POST"
},
"reviews": {
"href": "/shop/item/987/reviews",
"method": "GET"
}
}
}
```
### 5. 避免资源“类型”
不要在 API 中暴露资源的层级、权限或类型。客户端既不关心也不应该知道。例如,你不应该有 `/users/admin/123` 或 `/users/guest/123` 这样的资源。相反,应使用链接来定义可以对资源执行的操作。
### 6. 从书签(根资源)开始,让客户端探索
客户端必须从一个根 URL(或书签)开始,然后从那里导航你的 API 宇宙。
```json
{
"message": "Welcome to my RESTful API",
"_links": {
"users": {
"href": "/users",
"method": "GET"
},
"products": {
"href": "/products",
"method": "GET"
},
"orders": {
"href": "/orders",
"method": "GET"
},
"search": {
"href": "/search/?query={query}&page={page}",
"method": "GET",
"templated": true
},
"user-by-id": {
"href": "/users/{user_id}",
"method": "GET",
"templated": true
},
"product-search": {
"href": "/products/search/?name={name}&category={category}",
"method": "GET",
"templated": true
}
}
}
```
注意,多个链接使用了 `templated: true`,即我们在 HATEOAS 部分看到的 URI 模板:客户端甚至可以在不离开响应的情况下发现参数化端点。
现在你对如何构建 RESTful API 有了一个整体概念。结合 REST 原则、RESTful API 规则和 HATEOAS 概念,你将能够构建一个健壮的 API。
## 常见问题解答
### 如何表示 API 或端点的版本?
最常见的方法是将版本包含在 URL 中,例如 `/api/v1/users`。然而,如果你想遵循 RESTful 规则,应该将版本从 URI 中移除。最推荐的方式是使用 `Accept` 头进行协商,结构为 `application/vnd.example.v1+json`,其中 `example` 是你的 API 名称,`v1` 是版本。
例如,我的 API 名为 `dream`,最新版本是 `2.1`,那么头部将是:`Accept: application/vnd.dream.v2.1+json`。
另一个更简单但不太推荐的方法是使用自定义头部,如 `Api-Version`,其值为 `2.1`。也就是说,避免使用经典的 `X-` 前缀(`X-Api-Version`):自 2012 年起,[RFC 6648](https://www.rfc-editor.org/rfc/rfc6648) 已不鼓励使用该前缀。
### 如何处理分页?
你需要在响应中包含元数据,用 `meta` 字段指示分页信息。
一个分页示例如下:
```json
{
"data": [...],
"meta": {
"total": 150,
"page": 1,
"perPage": 10,
"hasNext": true,
"hasPrevious": false
},
"_links": {
"self": {"href": "/api/items?page=1", "method": "GET"},
"next": {"href": "/api/items?page=2", "method": "GET"},
"first": {"href": "/api/items?page=1", "method": "GET"},
"last": {"href": "/api/items?page=15", "method": "GET"}
}
}
```
其中:
- `total`:可用项目总数。不是当前页面的数量,而是整个集合的总数。
- `page`:当前页码。不能是 0 或负数。
- `perPage`:每页项目数。
- `hasNext`:指示是否还有更多页面。
- `hasPrevious`:指示是否存在上一页。
- `_links`:用于导航分页的链接,例如 `next`、`previous`、`first`、`last` 和 `self`。当某个链接不适用时(例如第一页没有上一页),则将其省略,而不是设为 `null`:这是 HAL 的做法,可以避免客户端的歧义。
你也可以使用模板让客户端指定页码:
```json
{
"_links": {
"page": {
"href": "/api/items?page={page}&perPage={perPage}",
"method": "GET",
"templated": true
}
}
}
```
### 如何处理错误?
这里有一个标准,尽管很少有人知道:[RFC 9457](https://www.rfc-editor.org/rfc/rfc9457)(HTTP API 的问题详细信息,它更新了 2016 年的 RFC 7807)。它定义了 `application/problem+json` 媒体类型,包含五个字段:`type`(标识问题类别的 URI)、`title`(简短、稳定的摘要)、`status`(HTTP 状态码)、`detail`(针对此具体情况的解释)和 `instance`(受影响资源的 URI)。并且允许你添加自己的扩展字段。
例如,返回 `404 Not Found` 状态码和 `Content-Type: application/problem+json` 头部的响应将包含以下正文:
```json
{
"type": "https://example.com/errors/article-not-found",
"title": "Article not found",
"status": 404,
"detail": "There is no article with id c9bb4e4a",
"instance": "/api/blog/c9bb4e4a"
}
```
如果你从头开始创建一个 API,我的建议是使用 Problem Details:这是标准,你的客户会感谢你。在我的项目中,我使用自己的约定(`type`、`errors`、`data` 和 `meta`),与我的用例响应结构集成;如果你好奇,我在《用 Python 实现整洁架构》(https://en.andros.dev/blog/c8838a2b/implementing-clean-architecture-in-python/) 一文中进行了说明。
## 结论
一个真正的 RESTful API,即 Fielding 所描述的那种,并不是 REST 的进化版:它是完整的 REST,是 Richardson 成熟度模型中几乎无人达到的第 3 级。虽然流行用法止步于资源、动词和状态码,但超媒体增添了将 API 转变为自描述、可导航界面的能力。
对于错误,你已经有标准 RFC 9457:使用它,你的客户将始终知道会发生什么。
简而言之,一个良好实现的 RESTful API 致力于可预测性、发现性和自描述性——这不仅仅是返回 JSON。
相似文章
如何对公共Web API进行版本管理?
关于API版本管理实践的讨论,质疑将URL路径(例如/v1/)与语义化版本控制耦合的做法,并探讨潜在的反模式。
为AI智能体设计API
本文认为,为AI智能体设计API需要遵循与人类不同的原则,强调清晰性、明确性,并避免使用默认值,因为智能体可以阅读整个文档并编写大量代码。
如果你只是坐在那里什么都不做,至少也要正确地什么都不做
这篇文章来自 The Old New Thing,解释了使API变得'惰性'的概念——以一种不破坏现有应用的方式'什么都不做'——并使用在Xbox上支持打印和淘汰widget API等例子。
AEPs: API增强提案
AEP项目为protobuf和HTTP REST API提供API设计规范和工具,托管在GitHub上。
API泛滥及其在AI社交媒体中的走向
本文揭示了主要平台(如X、Reddit和Stack Overflow)免费利用用户生成内容训练AI模型,随后对访问相同数据收取高昂API费用,并封禁抗议或删除自身内容的用户的模式。