什么是RESTful API

Hacker News Top 工具

摘要

本文解释了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。

相似文章

为AI智能体设计API

Hacker News Top

本文认为,为AI智能体设计API需要遵循与人类不同的原则,强调清晰性、明确性,并避免使用默认值,因为智能体可以阅读整个文档并编写大量代码。

AEPs: API增强提案

Hacker News Top

AEP项目为protobuf和HTTP REST API提供API设计规范和工具,托管在GitHub上。

API泛滥及其在AI社交媒体中的走向

Reddit r/artificial

本文揭示了主要平台(如X、Reddit和Stack Overflow)免费利用用户生成内容训练AI模型,随后对访问相同数据收取高昂API费用,并封禁抗议或删除自身内容的用户的模式。