跳转至

14.0 RESTful API

Purpose of APIs in Web Services

在设计网络服务时,API(Application Programming Interface,应用程序编程接口)的目的是将应用程序的逻辑和数据结构作为服务提供给其他开发者。这允许他们将这些功能嵌入到不同的应用程序中,并定制用户界面。API 是现代网络应用的关键构建模块。

传统的网络应用程序通常将后端逻辑、数据存储和前端图形用户界面紧密耦合在一起。这意味着如果要为移动设备(如 iOS 或 Android)或其他方式创建应用程序版本,就需要进行大量的重建工作。API 的出现解决了这个问题,它通过使用 HTTP 请求、标准化格式和文档化的响应类型,使后端能够服务于多种不同类型的客户端。

例如,Google Maps APIDropbox APIFacebook API 都是常见的 API 示例。它们允许开发者在自己的应用中集成地图、文件存储或社交媒体互动等功能,而无需从头构建这些复杂的系统。在课程中,我们可能已经接触过 API,比如用于从外部服务器获取随机可爱动物图片的 API。此外,在讨论客户端渲染时,实际上隐含地描述了一种私有 API,服务器用它与前端(浏览器)通信,以 JSON 格式发送数据,再由前端生成 HTML。

总之,API 对于网络的实际运行至关重要,现代网站几乎都会依赖其他服务器的 API 来提供功能,例如与通常位于不同服务器上的数据库交互、或使用第三方服务处理广告、跟踪或用户账户等。API 本身不是物理实体或程序,而是一套关于如何请求数据和格式化响应的标准和规则

Principles of REST APIs

REpresentational State Transfer (REST) 是一种网络架构风格,描述了与基于网络的资源进行交互的方式。REST 由 Roy Thomas Fielding 于 2000 年定义。他曾是 HTTP 协议的主要作者之一,其论文旨在明确网络的设计选择。

Fielding 提出了 REST 的六个高级别特性,这些特性并非强制执行,因此开发者对其解释不同:

  1. Client-server: 客户端和服务器的角色应明确区分,作为独立的进程运行,并通过传输层进行通信。实际上,它们通过 HTTP 接口进行通信,传输层是 TCP/IP。
  2. Layered System: 客户端和服务器之间不需要直接链接,可以通过中间节点(intermediate nodes)进行通信。客户端无需区分实际服务器和充当代理的中间层,服务器也无需知道是否直接与客户端通信。这封装了接口的抽象性,并通过代理服务器负载均衡器等机制实现了网络服务的可扩展性。
  3. Cache: 允许客户端或中间层缓存(cache)请求的响应,并在后续请求中直接提供缓存内容,而无需每次都回到服务器。这提高了网络的效率。服务器需要指定哪些内容可缓存(静态/动态)以及何时过期。加密内容通常难以被中间层缓存。所有网页浏览器都实现了缓存,以避免重复加载相同的静态文件(如图像、CSS 样式表)。
  4. Code on Demand (optional): 服务器可以在响应中提供可执行代码给客户端。这在网页浏览器中很常见,服务器提供 JavaScript 代码在客户端运行。然而,由于缺乏通用的可执行代码标准(如 iOS 不执行 JavaScript),此原则在 REST API 中并未真正普及。
  5. Stateless: 这是 HTTP 协议的关键属性,也与 REST API 紧密相关。服务器不应保留之前事务的任何记忆。客户端发出的每个请求都必须包含足够的信息,供服务器满足该请求。表示性的状态体现在 URL 或客户端请求的路由中,并通过每个请求发送。这使得服务易于扩展,因为负载均衡器可以将任意请求分派给任何服务器,它们之间无需通信。实际上,许多 REST API 为了实现会话管理会记录状态,但这通常是将状态存储在数据库等外部存储中,而不是存储在处理请求的单个服务器实例上。请求本身对服务器呈现为无状态,不依赖于该客户端之前的请求。
  6. Uniform Interface: REST 最重要也最模糊的要求是存在统一接口,这样客户端原则上无需针对特定服务器进行特别设计来消费其服务。统一接口包含四个方面:
    • Unique resource identifiers (唯一资源标识符): 每个资源都应该有唯一的标识符,即 URL。例如,访问特定用户的 URL 可能是 api/users/<id>
    • Resource representations (资源表示): 客户端和服务器之间交换的数据应该采用约定的格式,通常是 JSON。HTTP 支持内容协商,允许客户端指定期望的格式。
    • Self-descriptive messages (自描述消息): 客户端和服务器之间的通信应该清楚表明意图执行的操作。这通常通过结合 URL 和具有清晰语义的 HTTP 方法(如 GET, POST, PUT, DELETE)来实现。
    • Hypermedia links (超媒体链接): 客户端应该能够通过跟随响应中提供的超链接来发现新的相关资源。例如,获取新闻文章的响应中可能包含获取该文章评论的 URL 列表。

Standard Conventions for Designing REST APIs

设计 REST API 时,有一些标准约定和良好实践可遵循:

  1. 基于 CRUD 操作和 HTTP 方法: REST API 通常围绕 CRUD (Create, Read, Update, Delete) 操作设计,这些操作映射到标准的 HTTP 方法:

    • CREATE: 通常映射到 POST 方法。用于在集合中创建新资源。
    • READ: 通常映射到 GET 方法。用于获取单个资源或资源集合。
    • UPDATE: 通常映射到 PUTPATCH 方法。PUT 通常用于完全替换资源,PATCH 用于部分更新。
    • DELETE: 映射到 DELETE 方法。用于删除资源。
  2. URL 结构与数据模型对应: REST API 通常会为数据库中的每个模型或概念设计关联的 URL。URL 与 HTTP 方法结合时,应该自描述所执行的操作。例如,/api/movies 可能代表电影集合,/api/movies/{id} 代表具有特定 ID 的电影。并非数据库中所有部分都应通过 API 公开,特别是敏感或需要特定权限的数据。安全性通过 API 密钥认证来实现。

  3. API 文档: 为了让其他开发者了解和使用你的 API,必须提供详细的文档。文档应包括每个 URL 的列表,以及:

    • Purpose (目的): 该 URL 的用途。
    • Parameters (参数): 接受哪些参数。
    • HTTP Methods Accepted (接受的 HTTP 方法): 支持哪些 HTTP 方法(GET, POST 等)。
    • Response Data (响应数据): 返回什么数据,包括可能出现的标准错误代码及其含义。
    • Format of the Response (响应格式): 数据格式,例如 JSON。
  4. 良好实践:

    • Grouping related entities hierarchically (按层级分组相关实体): URL 结构应反映数据之间的层级关系。例如,文章的评论可以组织在文章 URL 下,如 articles/{article_id}/comments。这允许客户端只加载需要的最少数据,例如只加载文章标题而不同时加载所有评论。
    • Allowing pagination, sorting and filtering (支持分页、排序和过滤): 对于大型集合(如电影列表),API 应该支持分页(pagination)以分块加载数据,避免一次传输过多数据导致加载缓慢和带宽问题。服务器应处理排序(sorting)和过滤(filtering)操作,因为客户端可能没有完整的数据集来执行这些操作。
    • Versioning the API (版本控制 API): 由于 API 是公开资源,对其进行更改可能会破坏现有使用者。应通过在 URL 前添加版本号(如 v1/articles)或在请求头中指定版本来管理 API 版本。通常需要同时支持多个旧版本一段时间,以便用户迁移。
    • Use standard error codes (使用标准错误代码): 利用标准的 HTTP 状态码(如 200 OK, 201 Created, 400 Bad Request, 404 Not Found, 500 Internal Server Error)来表示操作结果和错误。避免使用自定义的错误码,这有助于与现有库和工具集成。
    • JSON Structure: API 请求和响应通常使用 JSON 格式传输数据。服务器端(如 Flask)可以提供将 Python 字典转换为 JSON 响应的方法(如 jsonify)。数据模型可以包含方法来方便地转换为字典或从字典加载数据。
    • Token-based Authentication: 对于需要授权的 API,常使用令牌(token)进行访问控制。登录用户可以生成令牌,并在后续请求中携带令牌以证明其有权访问 API。令牌不一定唯一标识用户,但表示其具有执行操作的权限。API 密钥是令牌的一种形式。令牌可以用于速率限制,防止滥用和拒绝服务攻击。

GitHub API 是一个详细的 REST API 示例。它的文档描述了各种资源(如 issues, repositories),以及如何使用 GET 请求 /issues 获取分配给认证用户的 issues 列表,支持过滤(按 assigned, created 等)和分页(per_page, page 参数)。响应示例会展示返回数据的结构(JSON),并提供状态码说明。

Differences between Designing a REST API and a Webserver

设计 REST API 与设计提供完整 Web 界面的 Web 服务器(webserver)存在关键差异:

  1. 耦合度:

    • Web Server (传统): 后端提供逻辑、数据存储,并负责生成和提供图形用户界面 (GUI) 给浏览器。这使得逻辑和表现层紧密耦合。
    • REST API: 后端只提供应用程序的逻辑和数据结构作为服务。它不提供或渲染 GUI。GUI 的生成和渲染工作由客户端(如 Web 浏览器中的 JavaScript 框架、移动应用程序、甚至是另一个 Web 服务器)负责消耗 API 数据后完成。
  2. 交付内容:

    • Web Server: 通常响应 HTTP 请求并返回 HTML、CSS、JavaScript 文件以及静态资源。
    • REST API: 主要通过 HTTP 请求/响应交换结构化数据,通常是 JSON 格式,而不是完整的网页文件。
  3. 错误处理:

    • Web Server: 遇到错误时,通常会渲染并返回一个包含错误信息的 HTML 页面给用户。
    • REST API: 遇到错误时,不会返回完整的网页,而是使用标准的 HTTP 状态码(如 400, 404, 500)并在响应体中包含格式化的错误信息(通常是 JSON)。
  4. 状态管理:

    • Web Server (传统): 虽然 HTTP 是无状态的,但 Web 服务器通常需要管理用户会话状态(如登录状态、购物车内容)。这可能需要在服务器端存储一些状态信息,这会增加服务器的复杂性和扩展难度。
    • REST API: 严格遵循无状态(Stateless)原则,服务器不存储客户端的会话状态。每个请求必须包含处理该请求所需的所有信息。状态通常保存在客户端或一个共享的外部存储(如数据库),所有服务器实例都可以访问它。这使得 REST API 更易于通过添加更多服务器实例进行水平扩展,因为任何请求都可以由任何可用服务器处理。

在一个框架(如 Flask)中,可以同时构建一个提供传统 Web 界面和 REST API 的应用程序。这通常通过使用蓝图 (blueprints) 等机制来分离 API 路由和 Web 路由来实现,从而实现逻辑上的分离,并可以在需要时将它们部署到不同的服务器实例上,同时共享同一个数据库或其他后端资源。

设计 REST API 的重点在于资源的定义、URL 的设计、HTTP 方法的正确使用、数据格式的约定以及清晰的文档。设计 Web 服务器的重点在于用户界面的呈现、用户交互流程、模板渲染以及会话管理等。REST API 更专注于作为数据和功能的服务提供者,而 Web 服务器则更专注于用户体验的呈现