RESTful API 设计:持久高质量的实践指南
在现代数字生态系统中,API 通常是产品的主要接口,但许多开发团队将其设计视为事后考虑,导致脆弱的集成和昂贵的重构。精心设计的 API 与糟糕设计的 API 之间的区别不仅仅是美学上的;它直接影响开发者的生产力、系统的可维护性以及服务本身的长期可行性。通过应用既定原则和前瞻性思维,您可以创建一个不仅满足当前需求,而且优雅地适应未来未知的接口。
您将学到什么
您将获得一个实用的、以决策为导向的框架,用于构建能够随着系统演变而生存和繁荣的 API。您将理解每个设计选择背后的权衡,从资源命名和 HTTP 方法使用到版本控制和错误处理,而不是记忆抽象规则。最后,您将能够自信地设计一个遵循最佳实践的 RESTful API,这些实践既标准又适应性强,确保您的服务成为未来开发的可靠基础。
核心原则:RESTful 设计的基础
REST(表述性状态转移)不是一种协议,而是一种由六个指导约束定义的架构风格。要设计一个遵循最佳实践的 RESTful API,您必须首先内化这些原则。
- 客户端-服务器分离: 这种解耦允许客户端和服务器独立演变。只要接口(API)保持稳定,您就可以更改底层数据库、身份验证机制或业务逻辑,而不会破坏客户端应用程序。
- 无状态性: 从客户端到服务器的每个请求必须包含理解和处理它所需的所有信息。服务器不存储任何会话状态。此约束增强了可靠性和可扩展性,因为任何服务器实例都可以处理任何请求。
- 可缓存性: 响应必须隐式或显式地定义自身为可缓存或不可缓存。适当的缓存可以显著减少延迟和服务器负载。
Cache-Control标头是您的主要工具。 - 统一接口: 这是 REST 的核心,也是其简单性的来源。它包括:
- 资源的标识: 资源(如用户、订单或产品)在请求中被标识,通常通过 URI。
- 通过表示对资源进行操作: 当客户端持有资源的表示(包括任何元数据)时,它有足够的信息来修改或删除它。
- 自描述消息: 每条消息包含足够的信息来描述如何处理它(例如,使用
Content-Type和Accept标头)。 - 超媒体作为应用状态的引擎(HATEOAS): 这通常是最容易被忽视的约束。HATEOAS 意味着客户端应完全通过服务器动态提供的超媒体链接来导航 API。虽然实现“纯粹”的 HATEOAS 可能很困难,但在响应中包含相关链接(如分页列表中的“next”链接)可以大大提高可发现性。
- 分层系统: 架构可以由分层组成(例如,安全、缓存、负载均衡)。这提高了整体系统的复杂性和安全性,因为每一层只需要知道下一层。
- 按需代码(可选): 服务器可以通过传输可执行代码(如 JavaScript)来扩展客户端功能。这在典型的 RESTful API 中很少使用。
1. 面向资源的设计:命名与结构
当您设计遵循最佳实践的 RESTful API 时,第一步也是最关键的一步是识别系统中的名词——资源。如何命名和构建它们为整个 API 定下了基调。
使用名词,而非动词
URI 代表一个资源。它应该是名词,而不是动作。
- ❌ 错误:
/getUser,/createOrder,/updateProduct - ✅ 正确:
/users,/orders,/products
对集合使用复数名词
集合是一组资源。使用复数形式以保持一致性。
- 集合:
/users - 实例:
/users/{userId} - 子集合:
/users/{userId}/orders
保持层次结构
资源自然形成层次结构。您的 URI 结构应反映这一点。例如,属于特定用户的订单可以嵌套。
GET /users/123/orders — 检索用户 123 的所有订单。
GET /users/123/orders/456 — 检索特定订单。
⚠️ 警告: 避免超过两到三层的深度嵌套。深度嵌套的 URI(例如
/users/123/orders/456/items/789)可能变得笨拙,并且通常表明您应该使用查询参数来扁平化资源结构。相反,考虑使用/items?orderId=456。Google AdInline article slot
2. 正确使用 HTTP 方法和状态码
API 的正确性在很大程度上取决于 HTTP 动词和状态码的正确使用。这是操作资源的机制。
HTTP 方法
- GET: 检索资源。应该是安全且幂等的。
- POST: 创建新资源。用于既不安全也不幂等的操作。通常,响应包含指向新创建资源的
Location标头。 - PUT: 在特定 URI 创建或替换资源。必须是幂等的。客户端发送完整的资源表示。
- PATCH: 部分更新资源。虽然默认情况下不严格幂等,但应设计为幂等。使用
PATCH和特定的媒体类型,如application/merge-patch+json,以避免竞态条件。 - DELETE: 删除资源。幂等:对同一 URI 的第二次 DELETE 应返回 404 或 204。
关键状态码
- 2xx 成功:
200 OK– 标准成功。201 Created– 资源已创建。包含Location标头。204 No Content– 成功,但无内容返回(常见于 DELETE)。
- 3xx 重定向:
301 Moved Permanently– 当 URI 永久更改时使用。304 Not Modified– 与缓存一起使用;表示资源未更改。
- 4xx 客户端错误:
400 Bad Request– 通用客户端错误(例如,格式错误的负载)。401 Unauthorized– 缺少或无效的身份验证。403 Forbidden– 已认证但未授权。404 Not Found– 资源未找到。422 Unprocessable Entity– 请求格式正确但语义无效(例如,验证错误)。
- 5xx 服务器错误:
500 Internal Server Error– 意外的服务器错误。503 Service Unavailable– 服务因维护或过载而停机。
3. 版本控制与演进
软件开发中唯一不变的是变化。设计良好的 API 必须有演进策略。当您设计遵循最佳实践的 RESTful API 时,如何处理版本控制将决定客户端的生命周期和可维护性。
拥抱向后兼容性
最稳健的策略是仅进行向后兼容的更改。这意味着:
- 您可以向请求或响应添加新字段。
- 您可以添加新端点。
- 您绝不应删除或重命名字段。
然而,有时破坏性更改是必要的。这时版本控制就变得至关重要。
版本控制策略
有几种方法可以对 REST API 进行版本控制。选择通常取决于组织的需求和现有基础设施。
| 策略 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| URI 路径 | /v1/users, /v2/users |
最明显,最容易实现,对开发者友好。 | 随着时间的推移可能导致 URI 膨胀。 |
| 查询参数 | /users?version=1 |
类似于 URI 路径但不太明显。 | 容易被遗忘,且不太符合习惯。 |
| 自定义请求标头 | Api-Version: 1 |
保持 URI 整洁。 | 需要自定义工具,可发现性较差。 |
| 内容协商 | Accept: application/vnd.myapp.v1+json |
符合 REST 风格,利用标准 HTTP。 | 实现和理解复杂。 |
基于行业趋势的综合(如 Stripe、Google 和 GitHub 等主要公共 API 所观察到的),URI 路径策略对于大多数团队来说仍然是最流行和最直接的。它明确且立即向开发者传达 API 版本。
弃用策略
如果您引入新版本并将旧版本标记为已弃用,请给客户端一个明确的移除时间表。在响应中包含 Deprecation 或 Sunset HTTP 标头。合理的 sunset 期可能是 12-24 个月,以便给开发者充足的时间迁移。
4. 文档的关键作用
API 的好坏取决于其文档。如果您的 API 完美但文档混乱或缺失,用户将挣扎,您的 API 将失败。OpenAPI 规范(以前称为 Swagger)是 API 文档的事实标准。
使用 OpenAPI
通过使用 OpenAPI 定义您的 API,您可以自动生成交互式文档(如 Swagger UI)、多种语言的客户端 SDK,甚至服务器存根。OpenAPI 文档作为单一事实来源。
实用文档要点
- 提供示例: 对于每个端点,提供真实的请求/响应示例。这比冗长的描述更有价值。
- 解释错误码: 列出每个端点所有可能的错误码及其含义。包含
problem+json结构(如 RFC 7807 所定义)以实现一致的错误处理。 - 包含入门指南: 快速入门指南帮助开发者在几分钟内成功进行首次 API 调用。
5. 安全与数据验证
安全不是事后考虑;必须从一开始就集成到设计中。当您设计遵循最佳实践的 RESTful API 时,安全是不可协商的支柱。
身份验证与授权
- OAuth 2.0 是授权的行业标准。它允许细粒度的访问控制。
- API 密钥 更简单但粒度较低。用于服务器到服务器的通信。
- JWT(JSON Web Tokens) 对于无状态身份验证很流行。
传输层安全(TLS)
始终对所有 API 端点强制执行 HTTPS。使用 TLS 1.2 或更高版本。这保护传输中的数据免受窃听和中间人攻击。
输入验证
永远不要信任客户端输入。在数据到达业务逻辑之前,在边缘验证所有传入数据。
- 白名单,而非黑名单: 精确定义允许的内容,而不是试图定义所有禁止的内容。
- 验证数据类型、长度和格式: 确保
string不是数字,email有效。 - 使用验证库: 在 Java 中使用 Hibernate Validator;在 Python 中使用 Marshmallow 或 Pydantic;在 Node.js 中使用 Joi 或 Zod。
6. 性能:缓存、分页与过滤
缓慢或难以查询的 API 会让用户沮丧。性能是核心设计关注点。
缓存
- 客户端缓存: 使用
Cache-Control标头告诉客户端可以缓存响应多长时间。例如,Cache-Control: max-age=3600缓存一小时。 - 服务器端缓存: 使用反向代理缓存(如 Varnish)或分布式缓存(如 Redis)来存储频繁的响应。
- E-Tags: 使用实体标签实现条件请求。服务器提供
ETag标头,客户端在后续请求的If-None-Match标头中使用它。如果资源未更改,服务器返回304 Not Modified,节省带宽。
分页与过滤
客户端绝不应被迫下载整个数据集。
- 分页: 使用
page和size或offset和limit参数。更稳健的方法是“基于游标”的分页(使用类似next_cursor的令牌),这对于大型数据集更高效,并防止数据更改时的重复。 - 过滤: 使用查询参数进行过滤。例如,
GET /products?category=books&price_min=10。- 一个强大的过滤标准是 OData 规范,但对于更简单的需求,自定义查询参数也可以。
- 稀疏字段: 允许客户端指定他们想要哪些字段,减少负载大小。例如,
GET /users/123?fields=id,name,email。
7. 测试与质量保证
将您的 API 视为产品。这意味着严格的测试。
单元测试
隔离测试您的业务逻辑。
集成测试
测试您的 API 如何与数据库、缓存和其他服务交互。
契约测试
随着微服务的兴起,契约测试(例如,使用 Pact)确保提供者(您的 API)和消费者(前端或其他服务)能够正确通信。它验证提供者满足消费者的期望。
端到端(E2E)测试
测试从客户端请求到数据库再返回的整个流程。
常见问题
1. 对于新 API,我应该使用 REST 还是 GraphQL?
REST 是直接、面向资源的 API 的绝佳选择,当您有定义良好的实体时。GraphQL 在数据高度互联或需要支持具有不同数据需求的多种客户端时非常有用。选择 REST 以获得简单性和缓存;选择 GraphQL 以获得灵活性和减少过度/不足获取。
2. 如何在 REST API 中处理部分更新?
执行部分更新的正确方法是使用 PATCH 方法。您的服务器应支持补丁媒体类型,例如 application/merge-patch+json(RFC 7396),它告诉服务器仅应用提供的字段。这比通过 PUT 发送整个资源更高效,并防止意外覆盖。
3. 处理错误并向客户端提供有意义反馈的最佳方式是什么?
使用适当的 HTTP 状态码(4xx 表示客户端错误,5xx 表示服务器错误)。在响应体中,返回一个一致的错误对象(遵循 RFC 7807),其中包含 type URI、title、detail 消息和 status。此结构为客户端提供程序化和人类可读的信息来解决问题。
4. 我应该多久更改一次 API 的版本号?
仅在您进行破坏性更改时引入新版本(例如,删除字段、更改数据类型或更改请求/响应结构)。非破坏性更改(如添加新字段或端点)应在现有版本内进行,以避免版本泛滥和开发者疲劳。
5. REST API 是否必须符合 HATEOAS?
虽然 HATEOAS 是 REST 的核心约束之一,但在实践中实现“纯粹”的 HATEOAS 很少见。对于高质量的 API,包含一些超媒体控制是有价值的,例如每个资源的 self 链接和分页链接(next、prev)。这提高了可发现性,而无需完整超媒体引擎的复杂性。
— Editorial Team
暂无评论。