在掌握了 REST 架构风格的理论基础之后,我们需要将这些抽象的原则转化为具体的设计决策。API 设计是一门需要在理论指导下不断实践和积累的技艺,它既需要对 REST 原则有深刻的理解,又需要考虑实际业务场景的复杂性和多样性。 一个设计精良的 API 能够让开发者在使用时感到自然和愉悦,而一个设计糟糕的 API 则会给使用者带来无尽的困扰和挫败感。
本节课我们将探讨 RESTful API 设计的核心策略和最佳实践,从资源建模的思维方式开始,逐步展开到 URI 设计、HTTP 方法使用、版本控制、错误处理等各个关键领域。 这些内容来源于数十年的行业实践经验,经过了无数项目的验证和打磨。无论你是正在设计第一个 API 的新手,还是希望提升现有 API 质量的资深工程师,这部分的内容都将为你提供有价值的指导。

资源建模是 RESTful API 设计的起点和核心。在开始编写任何代码之前,我们首先需要对业务领域进行深入分析,识别出其中的关键实体和它们之间的关系,然后将这些实体抽象为 API 中的资源。这个过程看似简单,实际上需要对业务有深刻的理解,并且能够从技术角度做出合理的抽象决策。
领域驱动设计(DDD)的思想在资源建模过程中具有重要的指导意义。DDD 强调与业务专家紧密合作,使用统一的语言来描述业务概念,这种语言应该同时被业务人员和技术人员所理解。当我们将这种思想应用到 API 设计时,资源的命名和结构应该反映业务领域的真实概念,而不是技术实现的细节。例如,在一个电子商务系统中,我们可能会有顾客、订单、商品、购物车等资源,这些名称直接来源于业务领域,任何了解电商业务的人都能够立即理解它们的含义。
资源建模的一个常见陷阱是将数据库表直接映射为 API 资源。虽然在简单场景下这种做法可能是合理的,但在复杂的业务系统中,数据库的结构往往是为了优化存储和查询而设计的,并不一定适合直接暴露给 API 消费者。一个良好的 API 应该提供业务导向的抽象,隐藏底层的实现细节。例如,用户的个人信息可能分布在多个数据库表中,但在 API 层面,我们可能希望将其作为一个统一的用户资源来呈现,让客户端不需要了解底层的数据分布情况。
确定资源的粒度是另一个需要仔细权衡的设计决策。粒度太细会导致客户端需要发起大量的请求才能完成一个业务操作,造成所谓的"过度获取"问题和不必要的网络开销。粒度太粗则可能导致响应中包含大量客户端并不需要的数据,或者使得资源的更新操作变得复杂和低效。
在实践中,资源粒度的选择应该以业务用例为导向。我们需要分析客户端最常见的使用场景,了解他们通常需要哪些数据的组合,然后据此设计资源的边界。如果某些数据总是一起被请求和更新,将它们合并为一个资源是合理的选择。如果某些数据有独立的生命周期和访问模式,将它们分离为独立的资源可能更加合适。
聚合的概念在资源粒度设计中也很有帮助。聚合是一组具有内在一致性要求的对象的集合,它们应该被视为一个整体来处理。在 DDD 中,聚合根是外部访问聚合内部对象的唯一入口。将这个概念应用到 API 设计,聚合根通常会成为一个独立的资源,而聚合内部的其他对象则可能作为这个资源的嵌套属性,或者通过子资源的形式来访问。例如,一个订单及其订单项可能构成一个聚合,订单作为聚合根成为主要资源,订单项则可以作为订单资源的一部分或者作为订单的子资源来访问。
业务实体之间的关系如何在 API 中表达,是资源建模中的另一个重要议题。实体之间可能存在一对一、一对多、多对多等各种关系类型,我们需要选择合适的方式在 API 中表现这些关系。
嵌套资源路径是表达从属关系的一种直观方式。当一个资源在逻辑上从属于另一个资源时,使用嵌套的 URI 结构可以清晰地表达这种关系。例如 /users/123/orders 表示用户 123 的订单集合,/users/123/orders/456 表示用户 123 的订单 456。这种设计的优点是关系表达清晰,缺点是可能导致 URI 过长,并且在某些场景下可能需要知道父资源的标识符才能访问子资源。
链接是表达资源关系的另一种重要机制。资源的表述中可以包含指向相关资源的链接,客户端可以通过这些链接来导航到相关资源。这种方式更加灵活,不要求资源之间存在严格的从属关系,也不会导致 URI 嵌套过深。例如,一个订单资源的表述中可以包含指向下单用户、配送地址、商品详情等相关资源的链接,客户端可以根据需要决定是否跟随这些链接获取更多信息。
在实际设计中,这两种方式往往是互补使用的。对于紧密耦合的从属关系,嵌套路径可能更加自然;对于松散的关联关系,链接则更加灵活。重要的是保持设计的一致性,让 API 的用户能够形成统一的心智模型。

URI 是资源在 Web 上的地址,也是 API 用户与资源交互的入口。一个设计良好的 URI 应该具有自解释性,让开发者在看到 URI 的瞬间就能理解它所指向的资源是什么。这种语义化的设计不仅提升了 API 的可用性,也有助于减少文档的依赖,使得 API 更加直观易用。
URI 应该使用名词而不是动词。这一原则直接来源于 REST 的资源导向思想。在 REST 模型中,操作是通过 HTTP 方法来表达的,而 URI 只负责标识资源。因此,我们应该使用 /orders 而不是 /getOrders,使用 /users/123 而不是 /getUserById。当你发现自己想要在 URI 中使用动词时,通常意味着你正在以 RPC 的思维来设计 API,需要退后一步重新思考如何将操作建模为对资源的标准操作。
URI 应该使用复数名词来表示资源集合。虽然关于使用单数还是复数一直存在争议,但使用复数已经成为更广泛接受的惯例。使用复数的理由是它能够保持 URI 的一致性:/users 表示用户集合,/users/123 表示集合中的特定用户,两者使用相同的资源名称,只是后者多了一个标识符。如果使用单数,我们会得到 /user 和 /user/123,虽然也能工作,但在语义上显得略有不一致。
URI 中的单词应该使用连字符(-)或小写字母加下划线(_)来分隔,而不是使用驼峰命名法。这是因为 URI 在某些系统中是大小写不敏感的,使用驼峰命名可能导致不同系统之间的行为不一致。连字符通常是更受推荐的选择,因为它在视觉上更加清晰,并且与 URL 的一般使用习惯一致。例如,我们应该使用 /order-items 而不是 /orderItems 或 /order_items。
URI 的层级结构应该反映资源之间的逻辑关系,但同时也需要控制嵌套的深度以保持 URI 的可管理性。过深的嵌套不仅使 URI 变得冗长难读,也可能给客户端带来不必要的复杂性,因为它们需要知道完整的资源层级才能构建正确的 URI。
一个广泛接受的经验法则是将 URI 的嵌套层次控制在三层以内。例如,/users/123/orders/456/items/789 已经是三层嵌套,再深入就会变得难以管理。如果业务模型确实需要更深的层级,我们应该考虑是否可以将某些子资源提升为顶级资源,通过查询参数或链接来表达关系。
另一个值得考虑的因素是资源是否可以独立于其父资源存在。如果一个资源具有全局唯一的标识符,并且可以在不知道父资源的情况下被有意义地访问,那么将其设计为顶级资源可能更加合适。以订单项为例,如果订单项有自己的全局唯一 ID,并且系统中存在直接通过订单项 ID 查询的需求,那么提供 /order-items/789 这样的直接访问路径是合理的,而不是强制要求通过 /users/123/orders/456/items/789 这样的嵌套路径来访问。
查询参数为 URI 提供了额外的灵活性,使得我们可以在不改变资源标识的情况下对请求进行修饰。查询参数最常见的用途包括过滤、排序、分页和字段选择等场景。正确使用查询参数可以显著提升 API 的功能性和用户体验。
过滤是查询参数最典型的应用场景。当客户端需要获取满足特定条件的资源子集时,可以通过查询参数来指定过滤条件。例如,/orders?status=pending 获取所有待处理的订单,/products?category=electronics&price_max=1000 获取电子产品分类中价格不超过 1000 的商品。过滤参数的命名应该直观清晰,让使用者能够立即理解其含义。
排序同样是查询参数的常见用途。客户端可能希望按照不同的字段对结果进行排序,并且可能需要指定升序或降序。一种常见的设计是使用 sort 参数,其值为排序字段名,通过前缀符号表示排序方向。例如,/products?sort=-price 表示按价格降序排列(减号表示降序),/products?sort=name 表示按名称升序排列。
字段选择允许客户端指定希望在响应中包含哪些字段,这在资源具有大量属性但客户端只需要其中一部分时特别有用。例如,/users/123?fields=id,name,email 只返回用户的 ID、姓名和邮箱,而不返回完整的用户资料。这种机制可以减少不必要的数据传输,提高响应速度,但也增加了服务端实现的复杂性。
查询参数应该只用于修饰请求,而不应该改变请求的根本语义。换言之,带有不同查询参数的请求应该访问的是同一个资源(或资源集合),只是返回的表述可能有所不同。如果查询参数的存在与否会导致访问完全不同的资源,那么应该考虑使用不同的 URI 路径来区分。
在第一章中我们已经介绍了 HTTP 方法的基本语义,本节将更深入地探讨如何在实际 API 设计中正确使用这些方法。正确使用 HTTP 方法不仅关乎 API 的 RESTful 程度,更直接影响到 API 的可预测性、可缓存性和整体设计质量。
GET 方法的使用看似简单,但仍有一些需要注意的细节。GET 请求应该是幂等且安全的,这意味着无论执行多少次,都不应该对服务器状态产生任何副作用。在设计 GET 请求时,我们需要特别注意那些可能产生副作用的隐式操作。例如,某些系统可能在资源被访问时更新"最后访问时间"字段,虽然这种副作用相对无害,但从严格意义上说已经违反了 GET 的安全性约束。更严重的例子是在 GET 请求处理中增加计数器或触发通知,这些都应该避免。
POST 方法的使用场景比其他方法更加多样。除了最常见的创建新资源外,POST 还可以用于那些不适合映射到其他方法的操作。例如,执行复杂的搜索查询时,如果查询条件过于复杂无法放在 URL 中,使用 POST 请求并在请求体中传递查询条件是合理的做法。同样,触发某个处理过程(如发送邮件、生成报告)通常也使用 POST 方法。在这些场景中,我们可以将操作本身建模为一个资源(如 "email-dispatches" 或 "report-generations"),POST 请求创建这个资源即表示触发了相应的操作。
PUT 和 PATCH 的选择是一个经常引起困惑的话题。PUT 的语义是完整替换目标资源的表述,这意味着 PUT 请求体应该包含资源的完整状态。如果请求体中缺少某个字段,服务器应该将该字段设为空值或默认值,而不是保留原有值。PATCH 的语义则是部分修改,请求体只需要包含要修改的字段。在实践中,PATCH 通常更加实用,因为客户端很少需要一次性更新资源的所有字段。然而,PATCH 请求体的格式需要能够清晰表达修改意图,JSON Merge Patch(RFC 7396)和 JSON Patch(RFC 6902)是两种标准化的格式选择。
将业务操作正确映射到 HTTP 方法是 RESTful API 设计的核心技能之一。大多数 CRUD 操作可以直观地映射到相应的 HTTP 方法:创建对应 POST,读取对应 GET,更新对应 PUT/PATCH,删除对应 DELETE。但实际业务中存在许多超出简单 CRUD 范畴的操作,如何处理这些操作是一个值得深入探讨的话题。
状态转换是一类常见的复杂操作。例如,订单可能需要从"待支付"转换到"已支付"状态,文章可能需要从"草稿"转换到"已发布"状态。一种处理方式是将状态作为资源的一个属性,通过 PATCH 请求来更新这个属性。例如,PATCH /orders/123 请求体包含 {"status": "paid"}。另一种方式是将状态转换建模为独立的资源或子资源,例如 POST /orders/123/payments 创建一个支付记录,成功创建支付记录的副作用是订单状态变为已支付。这两种方式各有优劣,选择取决于状态转换的复杂程度以及是否需要记录转换历史。
批量操作是另一类需要特别考虑的场景。RESTful API 的资源模型天然是面向单个资源的,但在实际应用中,批量创建、批量更新、批量删除等需求非常普遍。对于批量创建,可以向集合资源 POST 一个包含多个对象的数组。对于批量更新和删除,情况更加复杂,不同的 API 采用了不同的策略,我们将在后续章节中详细讨论这些模式。
异步操作也需要特别的设计考量。当一个操作需要较长时间才能完成时,让客户端同步等待显然不是好的体验。一种常见的模式是操作请求立即返回,响应中包含一个任务或作业资源的引用,客户端可以通过这个引用来查询操作的进度和最终结果。我们将在高级模式章节中深入探讨这种长时间运行操作的处理模式。
幂等性是分布式系统设计中的一个重要概念,对于 API 的可靠性有着深远的影响。一个幂等的操作意味着执行一次和执行多次的最终效果是相同的。在网络不可靠的环境中,客户端可能因为超时或网络错误而无法确定请求是否成功,此时客户端通常会选择重试。如果操作是幂等的,重试就是安全的,不会导致非预期的副作用。
GET、PUT、DELETE 按照 HTTP 规范都应该是幂等的。GET 只是读取数据不产生副作用,天然幂等。PUT 是完整替换资源,多次执行相同的 PUT 请求,资源的最终状态是相同的。DELETE 删除资源,即使资源已经不存在,删除操作也应该成功(可以返回 404 或 204),资源的最终状态都是"不存在"。
POST 通常不是幂等的,这是因为 POST 的语义是创建新资源或提交数据处理请求,每次执行都可能产生新的效果。然而,在某些场景下,我们可能需要使 POST 操作具有幂等性。一种常见的做法是引入幂等键(Idempotency Key)。客户端在发起 POST 请求时生成一个唯一的幂等键并包含在请求头中,服务器记录这个键与操作结果的映射。如果服务器收到带有相同幂等键的请求,它会返回之前的操作结果而不是重新执行操作。这种机制在支付等对重复操作敏感的场景中特别重要。

API 版本控制是一个在 API 设计中引起广泛讨论的话题。随着业务的发展和需求的变化,API 不可避免地需要演进和更新。然而,API 的变化可能会破坏现有客户端的兼容性,导致它们无法正常工作。版本控制提供了一种机制,使得新旧版本的 API 可以共存,给客户端足够的时间来适应变化。
在理想情况下,我们应该尽可能避免引入破坏性变更,通过向后兼容的方式来演进 API。添加新的资源、新的字段、新的操作通常是向后兼容的,因为它们不会影响现有客户端的功能。客户端可以简单地忽略它们不理解的新元素。然而,某些变化本质上是破坏性的,例如删除字段、修改字段类型、改变操作语义等,这些情况下版本控制就变得必要。
值得注意的是,REST 的创始人 Roy Fielding 对于在 URI 中包含版本号持批评态度。他认为这种做法违反了 REST 的原则,因为版本号成为了资源标识的一部分,而同一个资源不应该有多个标识符。他提倡使用内容协商机制来处理版本问题。然而在工业实践中,URI 版本控制因其简单直观而被广泛采用。理解这种理论与实践之间的张力,有助于我们在具体场景中做出合适的选择。
业界存在多种 API 版本控制的方案,每种方案都有其优缺点和适用场景。
URI 路径版本控制是最直观也是使用最广泛的方案。在这种方案中,版本号作为 URI 路径的一部分,例如 /v1/users 和 /v2/users 分别代表 API 的第一版和第二版。这种方案的优点是清晰直观,开发者可以从 URI 直接看出使用的是哪个版本,便于调试和文档编写。缺点是它将版本信息混入了资源标识,从严格的 REST 角度看不够纯粹,并且可能导致缓存问题(不同版本的同一资源被视为不同的资源)。
查询参数版本控制将版本号放在查询参数中,例如 /users?version=1。这种方案保持了 URI 路径的纯净,但可能与其他查询参数产生混淆,并且在某些场景下可能被缓存设施忽略。这种方案在实践中相对较少使用。
请求头版本控制使用自定义请求头或标准的 Accept 头来指定版本。例如,可以使用自定义头 X-API-Version: 1,或者使用带有版本信息的媒体类型 Accept: application/vnd.myapi.v1+json。这种方案从 REST 理论角度看更加纯粹,因为它将版本信息与资源标识分离,通过内容协商来实现版本选择。缺点是不够直观,增加了客户端的使用复杂度,且在浏览器直接测试 API 时不太方便。
主机名版本控制使用不同的子域名来区分版本,例如 v1.api.example.com 和 v2.api.example.com。这种方案提供了最彻底的隔离,不同版本的 API 可以部署在完全独立的基础设施上。缺点是运维复杂度较高,需要管理多个部署环境。
无论选择哪种版本控制方案,一些通用的最佳实践都值得遵循。
版本号的命名应该简单明了。虽然语义化版本(SemVer)在库和包的版本管理中非常有用,但对于 API 版本控制来说,通常只需要一个主版本号就足够了。只有在发生破坏性变更时才需要增加版本号,小的向后兼容的改进不需要新版本。过于频繁的版本更新会给客户端带来不必要的迁移负担。
版本的生命周期管理需要明确的政策。当新版本发布时,旧版本应该继续得到支持一段时间,给客户端足够的迁移时间。这个支持期限应该在 API 的文档中明确说明,可以是固定的时间(如发布后 12 个月)或者基于使用情况动态决定。在旧版本即将废弃时,应该提前通知客户端,可以通过邮件、文档更新或者在响应头中包含废弃警告等方式。
尽量减少破坏性变更是版本管理的核心原则。在引入可能破坏兼容性的变更之前,应该仔细评估是否有向后兼容的替代方案。例如,添加新字段而不是修改现有字段,废弃旧端点而不是直接删除,提供默认值使新参数变为可选等。这些策略可以延长 API 版本的生命周期,减少客户端的迁移成本。
错误处理是 API 设计中经常被忽视但极其重要的方面。当请求无法成功处理时,API 应该返回清晰、有用的错误信息,帮助客户端理解发生了什么问题以及如何解决。一个精心设计的错误响应可以大大减少客户端开发者的调试时间,提升整体的开发体验。
正确使用 HTTP 状态码是错误处理的基础。状态码提供了错误的分类信息,使客户端能够快速判断错误的性质。4xx 状态码表示客户端错误,意味着问题出在请求本身,客户端需要修改请求后重试。5xx 状态码表示服务器错误,意味着服务器在处理请求时遇到了问题,客户端可以稍后重试或联系支持团队。选择正确的状态码不仅关乎语义准确性,还影响到客户端的错误处理逻辑,例如某些客户端可能会自动重试 5xx 错误但不重试 4xx 错误。
除了状态码,响应体中应该包含更详细的错误信息。一个良好的错误响应通常包含以下要素:一个机器可读的错误代码,便于客户端程序化地处理不同类型的错误;一个人类可读的错误消息,描述发生了什么问题;可选的详细信息,如导致错误的具体字段、建议的解决方案等。某些 API 还会包含错误文档的链接,指向更详细的错误说明和解决指南。
错误响应的格式应该在整个 API 中保持一致。无论是哪个端点返回的错误,响应的结构都应该是相同的,这样客户端可以用统一的逻辑来处理所有错误。建立一个标准的错误响应模式并在 API 设计规范中明确定义是一个好的实践。
在错误响应中要注意安全性考量。错误消息不应该暴露敏感的内部实现细节,如数据库查询语句、完整的堆栈跟踪、内部服务器地址等。这些信息可能被恶意用户利用来寻找系统漏洞。对于生产环境,应该使用通用的错误消息,详细的调试信息只在开发环境中返回。
不同类型的错误需要不同的处理策略和响应格式。让我们看看几种常见的错误场景及其处理方式。
验证错误是 API 中最常见的错误类型之一。当客户端提交的数据不满足验证规则时,API 应该返回清晰的信息指出哪些字段有问题以及具体是什么问题。对于这类错误,通常使用 400 Bad Request 或 422 Unprocessable Entity 状态码。响应体应该包含一个错误列表,每个错误项指明出错的字段名和具体的错误描述。例如,如果用户注册时邮箱格式不正确且密码太短,响应应该同时报告这两个问题,而不是只报告第一个问题,这样用户可以一次性修复所有问题。
认证和授权错误需要明确区分。401 Unauthorized 表示客户端未提供有效的认证凭据或凭据已过期,客户端应该(重新)进行身份验证。403 Forbidden 表示客户端已经通过身份验证,但没有权限执行请求的操作。这两个状态码的区分对于客户端的错误处理逻辑很重要:401 通常触发重新登录流程,而 403 则表示需要更高的权限或该操作对当前用户不可用。
资源不存在错误应该返回 404 Not Found 状态码。需要注意的是,有时候返回 404 可能会泄露信息,让攻击者知道某个资源 ID 不存在从而推断出有效的 ID 范围。在安全敏感的场景中,可能需要考虑对未授权的访问也返回 404 而不是 403,避免确认资源的存在性。
并发冲突发生在多个客户端同时尝试修改同一资源时。使用乐观锁机制时,如果客户端提交的版本号与服务器当前版本不匹配,应该返回 409 Conflict 状态码。响应中可以包含当前的资源状态,让客户端决定如何解决冲突。
RFC 7807 定义了一个称为"问题详情"(Problem Details)的标准格式,用于在 HTTP API 中表达错误信息。这个规范提供了一个结构化的错误响应格式,正在被越来越多的 API 所采用。
问题详情格式包含几个标准字段:type 是一个标识错误类型的 URI 引用,可以指向描述该错误类型的文档;title 是错误类型的简短人类可读摘要;status 是 HTTP 状态码;detail 是针对此次错误发生情况的人类可读解释;instance 是标识此次特定错误发生的 URI 引用。除了这些标准字段,规范还允许添加自定义字段来传递额外的错误信息。
采用标准化的错误格式有多方面的好处。对于 API 提供者,它提供了一个经过深思熟虑的设计模板,减少了设计决策的负担。对于 API 消费者,它使得不同 API 的错误处理逻辑可以更加统一,降低了学习成本。对于工具生态,标准化的格式使得可以开发通用的错误处理库和工具。
当资源集合可能包含大量元素时,分页是必不可少的功能。没有分页的 API 在数据量增长时会遇到严重的性能问题,不仅服务器需要处理大量数据,网络传输也会变得缓慢,客户端解析大量数据也会消耗过多资源。设计合理的分页机制可以确保 API 在各种数据规模下都能保持良好的性能。
偏移量分页是最传统也是最容易理解的分页方式。客户端通过 offset(或 skip)和 limit(或 page 和 page_size)参数来指定要获取的数据范围。例如,/products?offset=20&limit=10 表示跳过前 20 条记录,获取接下来的 10 条。这种方式实现简单,客户端可以随机访问任何页面,但它也有明显的缺点。当偏移量很大时,数据库需要扫描并跳过大量记录,性能会显著下降。同时,如果在分页过程中有新数据插入或删除,可能导致某些记录被重复获取或遗漏。
游标分页使用一个指向特定记录的游标(通常是经过编码的记录标识符或时间戳)来标记当前位置。客户端在获取下一页时,提供上一页响应中返回的游标作为起点。例如,/products?cursor=eyJpZCI6MTAwfQ== 表示从游标指向的位置开始获取数据。这种方式在处理大数据集时性能更好,因为数据库可以直接定位到游标位置而不需要扫描前面的记录。它也能更好地处理数据变化的情况,确保不会遗漏记录。缺点是客户端无法随机访问特定页面,只能顺序向前或向后翻页。
键集分页(也称为"seek 方法")是游标分页的一种变体,使用有序字段的值作为翻页依据。例如,按创建时间倒序排列时,可以使用 /products?created_before=2024-01-01T12:00:00Z 来获取指定时间之前创建的产品。这种方式的性能优势与游标分页类似,但游标值更加透明,便于调试和理解。
分页响应中应该包含足够的元数据,帮助客户端理解当前的位置和整体情况。常见的元数据包括总记录数、总页数、当前页码、每页大小,以及导航到上一页、下一页、首页、末页的链接。
总记录数的计算在大数据集上可能是昂贵的操作,某些数据库需要全表扫描才能得到精确的计数。因此,有些 API 选择不返回精确的总数,而是返回一个近似值或者简单地指示"是否有更多数据"。这种权衡需要根据具体业务需求来决定。
遵循 HATEOAS 原则的 API 会在响应中包含翻页导航的链接。这些链接通常放在响应的元数据部分或者使用 Link 响应头。例如,响应可能包含 next、prev、first、last 等链接,客户端可以直接使用这些链接而不需要自己构造翻页 URL。这种设计使得服务端可以灵活改变分页的实现方式而不影响客户端。
除了分页,对资源集合进行过滤和搜索也是 API 常见的需求。过滤通常指根据资源的特定属性值来筛选结果,而搜索则通常涉及更复杂的条件匹配,如全文搜索。
简单的等值过滤可以直接使用查询参数表达。例如,/orders?status=pending&customer_id=123 获取客户 123 的所有待处理订单。这种方式直观易懂,适合大多数简单的过滤场景。
更复杂的过滤条件需要设计专门的查询语法。例如,数值范围过滤可以使用后缀表示法:price_min=100&price_max=500 或者 price_gte=100&price_lte=500。日期范围过滤类似:created_after=2024-01-01&created_before=2024-12-31。某些 API 设计了更强大的查询语法,甚至允许类似 SQL 的过滤表达式,但这增加了实现复杂度和安全风险(需要防止注入攻击)。
全文搜索通常通过一个通用的搜索参数来触发。例如,/products?q=wireless headphones 搜索包含关键词的产品。全文搜索的实现通常需要专门的搜索引擎(如 Elasticsearch)支持,简单的数据库 LIKE 查询在大数据集上性能较差。

规范确保了不同开发者、不同团队开发的 API 具有一致的风格和行为,为 API 消费者提供了可预测的使用体验。没有规范的情况下,每个开发者可能按照自己的理解和偏好来设计 API,导致整个 API 生态杂乱无章,增加了学习成本和维护难度。
规范不仅是技术文档,更是团队知识的积累和共识的体现。通过制定规范的过程,团队成员可以就各种设计决策进行讨论和辩论,最终形成大家认可的最佳实践。新加入团队的成员可以通过学习规范快速了解团队的设计理念和约定,减少了口头传承的依赖和信息丢失的风险。
许多大型科技公司都有公开的 API 设计规范,如 Google Cloud API Design Guide、Microsoft REST API Guidelines、Heroku HTTP API Design Guide 等。这些规范经过了大规模实践的验证,是非常有价值的参考资料。然而,直接照搬这些规范可能并不适合所有场景,团队应该根据自己的业务特点和技术栈进行适当的调整和取舍。
一份完善的 API 设计规范通常涵盖以下几个方面。
URI 设计规则定义了资源的命名惯例、路径结构、查询参数的使用方式等。这部分应该明确规定使用复数还是单数名词、单词如何分隔(连字符还是下划线)、嵌套层次的限制、版本号的位置等具体问题。
HTTP 方法使用规则明确了每种 HTTP 方法的使用场景和预期行为。例如,规定 GET 请求不应该有请求体,POST 请求成功创建资源后应该返回 201 状态码和资源的表述,DELETE 请求成功后应该返回 204 还是返回被删除资源的最后状态等。
请求和响应格式规范定义了通用的数据结构和字段命名约定。这包括日期时间的格式(推荐 ISO 8601)、布尔值的表示、空值的处理、分页响应的结构、错误响应的格式等。字段命名应该采用一致的大小写风格,如驼峰命名法(camelCase)或蛇形命名法(snake_case)。
认证和授权规范说明了 API 使用的认证机制(如 JWT、OAuth 2.0)、认证凭据的传递方式、授权失败的响应格式等安全相关的约定。
错误处理规范定义了错误响应的标准格式、错误代码的命名规则、常见错误场景的处理方式等。统一的错误处理使得客户端可以用一致的逻辑处理来自不同端点的错误。
规范制定出来只是第一步,更重要的是确保规范得到执行。代码审查是保证规范执行的重要手段,在审查过程中应该检查 API 设计是否符合团队规范。自动化工具可以进一步提高执行效率,如使用 linting 工具检查 OpenAPI 规范文件是否符合设计规则。
规范应该是活的文档,随着团队经验的积累和技术的发展不断演进。当遇到规范未覆盖的新场景时,应该及时更新规范以填补空白。当发现现有规则不合理或有更好的实践时,也应该组织讨论并更新规范。每次更新都应该记录变更原因和时间,保持规范的变更历史。
规范的更新需要谨慎处理向后兼容性。如果新规范与现有 API 不一致,需要制定迁移计划,决定是逐步改造现有 API 还是只将新规范应用于新开发的 API。无论采取哪种策略,都应该确保规范文档清晰地说明了当前的状态和未来的方向。
API 文档是 API 产品的重要组成部分,其质量直接影响着开发者的使用体验和 API 的采用率。即使是设计最精良的 API,如果没有清晰的文档,开发者也难以有效地使用它。相反,一个设计尚可但文档优秀的 API,往往比设计优秀但文档糟糕的 API 更受开发者欢迎。
优秀的文档应该服务于开发者的整个使用旅程。对于刚接触 API 的开发者,需要快速入门指南帮助他们在最短时间内完成第一次成功调用。对于正在集成 API 的开发者,需要详细的参考文档说明每个端点的功能、参数、响应格式等。对于遇到问题的开发者,需要故障排除指南和常见问题解答。对于希望深入了解的开发者,需要概念解释和架构说明。
文档应该始终与 API 的实际行为保持同步。过时的文档比没有文档更糟糕,因为它会误导开发者,导致他们在错误的方向上浪费时间。采用"文档即代码"的实践,将文档源文件与 API 代码放在同一个仓库中,在代码变更时同步更新文档,可以帮助保持文档的时效性。
OpenAPI 规范(原 Swagger 规范)是描述 RESTful API 的行业标准。它使用结构化的格式(YAML 或 JSON)定义 API 的端点、参数、请求体、响应、认证方式等所有细节。这种机器可读的规范格式带来了多方面的好处。
首先,OpenAPI 规范可以作为 API 设计的蓝图。在编写任何代码之前,团队可以先用 OpenAPI 格式定义 API 的接口,就接口设计进行讨论和评审。这种"设计优先"的方法有助于在早期发现设计问题,避免在实现后才发现需要大幅修改。
其次,从 OpenAPI 规范可以自动生成多种形式的产物。Swagger UI 等工具可以生成交互式的 API 文档,开发者可以直接在文档页面测试 API 调用。各种代码生成器可以从规范生成客户端 SDK,支持多种编程语言。服务端的请求验证逻辑也可以基于规范自动生成。
此外,OpenAPI 规范为 API 生态中的各种工具提供了统一的接口。API 网关可以基于规范配置路由和验证规则,测试工具可以基于规范生成测试用例,监控工具可以基于规范分析 API 的使用情况。这种工具链的互操作性大大提高了 API 开发和运维的效率。
开发者体验(DX)是衡量 API 质量的重要维度,它关注的是开发者在使用 API 时的整体感受,包括学习曲线、集成难度、调试便利性等方面。提升开发者体验需要从多个角度入手。
提供可运行的代码示例是提升开发者体验的有效手段。与其只给出抽象的参数说明,不如提供完整的、可以直接复制运行的代码片段。代码示例应该覆盖常见的使用场景,使用流行的编程语言和框架,并确保代码是经过测试可以正常工作的。
交互式的 API 探索工具让开发者可以在不写代码的情况下尝试 API。Swagger UI 提供了基本的交互能力,而一些更先进的开发者门户可以提供保存请求历史、环境变量管理、团队协作等高级功能。Postman 等第三方工具也是开发者常用的 API 探索和测试工具,提供官方的 Postman Collection 可以方便开发者快速上手。
当开发者遇到问题时,错误响应应该提供足够的信息帮助定位原因。在开发环境中,可以提供更详细的错误信息和调试数据。日志追踪机制可以帮助开发者和支持团队追踪特定请求的处理过程,快速定位问题。
持续收集开发者反馈并据此改进 API 和文档是提升开发者体验的关键。可以通过多种渠道收集反馈,如开发者社区、技术支持工单、用户调研等。定期分析常见问题和痛点,并将改进措施纳入产品路线图。记住,API 的最终用户是开发者,他们的使用体验直接决定了 API 的成功与否。
这部分我们一起梳理了 RESTful API 设计的核心思想,从如何把业务需求抽象为资源、合理设计粒度和关系,到路径语义、嵌套层级与查询参数的使用;也涵盖了版本管理、错误响应、分页与过滤的各种实践方法。 通过这些原则和工具,不仅能帮助我们制定和落地团队规范,还能提升开发者的使用体验。掌握这些理念,是打造高质量、可持续演进 API 的坚实基础。
接下来,我们将会继续深入学习业界沉淀下来的常见设计模式,让你面对各种实际场景时有据可依、设计 API 更加得心应手。
