17611538698
webmaster@21cto.com

RESTful API 详解与最佳实践

作者 万能的大雄 分类 管理/领导力 01月10日




概述


REST最初是由 Roy Fielding 在 2000 年制定的规范,它启发了现代软件 API 的设计标准。


尽管大多数人都熟悉 REST、REST-like和RESTful等术语,但它们之间的区别可能每个人并不一定都十分清晰。


简而言之,REST 是一种架构风格,而 REST API 是实现它的 Web 服务。但是更复杂的是,开发人员使用CRUD样式调用 HTTP Web API,该样式主要的REST 实现(但不是 100%)是RESTful服务。


换句话说,我们不能很简单一句话总结:“REST是名词而RESTful是形容词。”


那么,这些术语可以互换使用吗?大多数情况下可以。下面我们就仔细研究 REST 和 RESTful 的起源和推荐用法。本文还将详细说明 REST 术语之间的细微差别。


REST的定义与发展史


让我们从基础开始。REST 的全称为 “REpresentational State Transfer”,是一种由各种原则组成的架构风格。其特性总结如下:


  • 无状态:REST 应用程序必须是无状态;

  • 客户端-服务器模式 (分离):能够独立修改这些组件是必不可少的;

  • 分层系统:REST 系统的组件不能超越自己的所在层;

  • 可缓存:REST 服务器数据须标记为可缓存或不可缓存;

  • 统一接口:组件之间强调统一接口。


Roy Fielding 在 2000 年的论文中详细描述了 REST,在之后一些开发者在一段时间将它称为并视为“SOAP 杀手”。它被设计为比SOAP更易于使用和更灵活,eBay 和 Amazon 在理论早期就采用了它,这也给 REST 的普及铺就了一个光明未来。


事实上 ,REST 已经成为 API 领域的事实主流架构风格。


如果你在 2022 年开发 API,除了极少数个例外,绝大多数都会采用REST风格,但是开发者需要对REST API 设计以及实际开发时要进行透彻理解。


在软件开发中,还有有很多 API 最佳实践。如下示例:


  • 使用 URI 从服务器访问资源

  • 使用 HTTP 协议(使用 GET、POST、PUT 和 DELETE 等动词相当于数据库的 CRUD 操作)

  • 以JSON或XML、Atom、OData等文档结构返回结果


以上均与 REST 相关联,但并不是所有内容都已经由 Fielding 明确定义。而实际的情况是,组成 REST 的大部分内容定义一直是比较松散的。




RESTful 的定义


如前面内容所述,被描述为 RESTful 的 API 它采用并遵守 REST 架构的原则,但它可能并不符合 HATEOAS 的约束——Roy Fielding在 2008 年的论文认为 REST API 必须是超文本驱动。


我们会看到一些开发者宣称他们的服务是基于 REST 的或 REST 式的,这通常是遵循 REST 概述的一些基本原则(但不是全部)的简写。正如上面看到的,RESTful 更可能用于描述遵循所有或几乎所有 REST 原则的服务。


顺便说一句,服务可能需要偏离 REST 架构的原因有很多,因此没有必要将基于 REST 的架构视为低劣或无效的。然而我们值得看看它遵循/不遵循哪些原则以及这可能如何影响实施。


早在 2017 年,83% 的公共 API 是“REST API”,而15% 是 SOAP API。但在这种情况下使用该术语具有误导性。因为 REST 指的是架构约束,并且与语言和平台无关,所以没有简单明了的“REST API”应该是什么样子的图景。上面的数字可能还包括 RESTful 和 REST-ish/基于 REST 的服务。


即便如此, REST API 开发也已经在该领域取得(并保持)了主导地位。很明显,它的主要的对手不再是 SOAP,而是更新式更流行的GraphQL API。


REST/RESTful 的未来


目前有 82% 的 API 使用的 RESTful(OpenAPI/Swagger),21% 是 RESTful(没有用 OpenAPI/Swagger),而 19% 采用了 GraphQL。


需要说明是,GraphQL 并不是 REST 的直接“竞争对手”,因为它是一种语言而不是一种架构风格。尽管如此,自 Facebook 推出以来,它一直被一些开发者吹捧为 REST API 的替代品。


当然,也有大量的“和平休战”式的技术文章。公平地说,这是很好的文字游戏。他们中的许多人推荐,GraphQL 将让 REST 变得无力抵抗。


但现在已经是 2022年,上面人们说的那种情况,现实中并没有发生。


根据公开引用的数字,公共 API 中 REST 架构的使用仅下降了一个百分点左右。因为彻底检查 API 需要大量的代码重构,这肯定不是一件容易的事。


畅想 GraphQL 或其它一些令人激动的 API 开发新方法,是否会在未来几年取代 RESTful 还有待观察。就目前而言,REST 仍然是占比很高的开发方式,还没有必要拆除所有的 RESTful API。


Rest API 最佳实践标准文档可以帮助开发者在微服务设计上提供帮助。我在下列内容描述用什么样的方式构建微服务标准。


URI格式


完整的URI 格式是这样的:

{base-path}/{area}/{version}/entity1/{entity1}/{entity2}


其中:


  • 基本路径是 {dns-name}/{microservice-name}

  • area api 或 management 等表示区域,大多数情况下只使用api

  • version是微服务的当前版本

  • entity1是提供的网关 ,称为entity1

  • entity2是服务的主要实体


实体名称应为:

  • 使用复数和名词;

  • 对复合名称使用破折号,例如:-customers-orders。


如果子实体是服务的主要实体,则可以删除实体名称。

例如:order/statuses而不是order/order-statuses。


如果子实体不是服务的主要实体,则应使用完整的子实体名称。

例如:customers/order/order-statuses。


如果节点未按customers分隔,则 URI 的 customers/{customer-id} 部分将被删除,变为如下:

{base-path}/{area}/{version}/{entity}


比如,订单服务示例:

  • https://base-path/order-service/api/v1/customers/10/order


字段命名规则:

  • 所有字段名称都将是驼峰式的;

  • 字段名称应该不包含空格或下划线;

  • 复合字段名称应重复对象名称,除非它们专门用于实体本身;

  • 如果 status在订单对象上,则可以使用status代替orderStatus;

  • 字段名称尽可能区分大小写。orderstatus与 order Status 不同;

  • 不要有仅大小写不同的字段(订单状态和OrderStatus);

  • 不要使用缩写,除非它是行业通用术语;

  • 如果是全名,则总是小写缩写,如果是复合字段名称的一部分,则总是小写;

  • 用/id或/statusId 而不是 /iD或/statusID

  • 字段引用应始终以 ID 结尾,除非它们返回实际对象。如下:

    # 获取订单响应示例 Get /order-service/v1/orders/12 { "id": "12", "status": { "id" : "657", "name" : "Open" }, "contactId" :“9”}

各个操作(POST、PATCH、GET)之间的字段名称应该一致。


但是,可能会有一些字段在各种情况下未遵守规则或返回。可以选择不接受 POST 的状态/名称,而使用 GET 方式返回。


API 返回详情:


  • 如果字段为空,不要在响应中返回;

  • 如果字段为空(例如空字符串),则应将其视为 NULL;

  • 例外情况是 NULL 和空值之间的含义存在差异。例如,0.00 的成本可能与 NULL 的未设置成本不同;

  • 如果一个字段是一个没有值的数组,你应该返回一个空数组而不是不返回该字段;

  • 日期时间应使用 ISO 标准表示。


HTTP 方法和 HTTP 状态代码


通用状态代码,适用于所有 HTTP 动词https://en.wikipedia.org/wiki/List_of_HTTP_status_codes。


其中,在某些情况下可以在应用程序中使用的很少,如下所示 -

状态代码附加信息 503(服务不可用)使用此统计代码来说明电路是否打开(如果应用程序正在使用断路器)409(冲突)如果由于任何依赖性而无法执行操作。例如如果试图删除一个不能删除的资源,除非该资源的所有引用都被明确删除。


401(未授权)收到一个请求,但身份验证令牌无效来执行该请求,通常是在 CUD 操作的情况下。


GET


仅用于检索资源表示/信息。
它应该是只读的,不应修改表示/信息,因此被认为是安全的方法。GET 请求不应在请求正文中包含任何内容。


请求:标题

响应:请求 URI 处的资源或集合的表示


状态码说明


200(OK)

已成功检索资源。如果您尝试获取资源列表但没有资源列表,那么它仍然是 200。
通常返回的数组仍应返回,但它的长度为 0。

400(错误请求)

传入了无效的查询字符串值,例如格式不正确或服务器允许的最大值的限制。或者,如果请求缺少作为有效请求的强制性查询字符串或标头。

404(未找到)

请求的资源不存在。仅适用于获取 1 个资源,例如通过其 ID 获取它,或者如果路径中的某个 ID 肯定是未知的。
例如,如果您得到一个不正确的 ID,您的服务没有验证,那很好,它不应该导致 404。

邮政

POST 方法用于创建新资源。它既不是安全的也不是幂等的。调用两个相同的 POST 请求可能会导致两个不同的资源包含相同的信息(资源 ID 除外)。
请求:资源的表示。

响应:创建的资源的主体和/或定位新资源的 URI

注意:POST 不应返回 404 响应状态代码。除非在本文档的其他地方明确提及,否则 URL 中不应允许查询字符串。

201 (Created)如果资源创建成功409 (Conflict)如果资源已经存在


批量请求

仅支持 POST 请求,但允许在一个请求中创建多个相同类型的资源。此类 API 必须带有 /batch 后缀,并且请求主体应为资源数组。


请求:一个资源数组。下面是一个例子:

[  {     "name": "Raj",     "taxId": 10,     "mainDetail": {         "number": "12345"     }   },   {     "name": "Jack",     "taxId": 10,     "mainDetail": {         “号码”:“3457”     }   } ]

响应:返回对象数组,包含请求主体的每个资源的信息(和状态代码)


以下是响应正文格式和部分成功的示例。响应包含两个对象,其中一个成功,另一个失败,状态码为 400:

[{   "status": 200,   "result": {     //result   } }, {   "status": 400,   "errors": [{     //errors   }] }]

你可以很好地包含 Id 或根据 API 需要确定的任何标识,这有助于理解响应主体数组中每个对象到请求有效负载的映射。


注意:微服务应该对每个请求允许的资源数量提出并实施限制

注意:如果所有单个资源请求都返回相同的状态代码,则请求作为一个整体应该返回该状态代码。


状态码


201(创建)

如果所有请求都成功 207 (多状态,Multi Status) 包含多个单独的响应代码,取决于发出的子请求的数量。

400(错误请求)

请求负载无效。

注意:微服务可能需要一些额外的 GET、POST 端点来支持相关功能,例如 ping、更新某些内容的到期时间或与远程过程调用相关的任何内容。尽管它们不属于 RESTful 架构,但可以支持它们,并且此类请求的端点应与资源节点不同。

PUT

PUT 不是安全的,而是幂等的请求。一次又一次地进行相同的 PUT 调用,不会改变服务器上资源的状态。

它创建一个新资源或修改现有资源。如果一个 URL 支持 PUT 和 POST 两种请求,那么 PUT 请求的负载应该与 POST 完全相同。可以很好地选择只支持 PUT 请求,而不是同时支持 PUT 和 POST。如果 URL 的性质是客户端不知道资源是否存在但仍想更新或创建资源,那么 URL 应该只支持 PUT 请求。

在成功响应中返回修改后的资源(对于状态代码 200)如果有一个强大的用例,否则它应该只返回状态代码而没有响应正文。

请求:资源的表示。
响应:更新的一般状态,但我们是否也应该返回修改后的资源(或创建的)?

注意:URL 中不允许有查询字符串。

状态码说明

200(好)

如果资源已成功更新。201(已创建)在成功创建新资源的情况下。204(无内容)如果资源已成功更新且未返回任何表示。404(未找到)如果客户端尝试更新不存在的资源。

PATCH

PATCH 方法请求将请求实体中描述的一组更改应用于由 Request-URI 标识的资源。

请求:一个“补丁文档”,表示要应用于指定资源的一组更改。与 PUT 不同,PATCH 不是幂等请求。因此,如果为服务器上不存在的资源发出 PATCH,它应该返回 404 而不是创建该资源。

使用具有以下格式的 JSON-PATCH 并按照 RFC 标准https://tools.ietf.org/html/rfc6902并且请求的内容类型应为 Content-Type: application/json-patch+json

[{ "op": "test", "path": "/a/b/c", "value": "foo" },      { "op": "remove", "path": "/a/b/ c" },      { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },      { "op": "replace", "path": "/a/b/c", "value": 42 },      { "op": "move", "from": "/a/b/c", "path": "/a/ b/d" },      { "op": "copy", "from": "/a/b/d", "path": "/a/b/e" } ]

响应:更新的状态。

注意:URL 中不允许出现查询字符串。参考http://jsonpatch.com/中关于 JSON Patch。


为简单起见,我们将仅支持“替换”操作来替换资源中任何现有字段的值。如果适用和需要,你还可以支持添加/删除操作,但仅限于类似数组的字段。无需使用 JSON Patch 添加或删除新字段。


如果尝试修补只读字段,则应该抛出 400 错误。


批量删除


一个 POST 请求,允许在一个请求中删除多个相同类型的资源。此类 API 必须带有 /batch-delete 后缀,并且请求正文应为要删除的 id 数组。

请求:一个 id 数组。下面是一个例子:

[   1,   2,   5 ]

Response : 一个响应对象数组,包含请求主体的每个资源的信息(和状态代码)

以下是响应正文格式和部分成功的示例。响应包含两个对象,其中一个成功,另一个失败,状态码为 400:

[{     "status": 200,     "result": {         //result     } }, {     "status": 400,     "errors": [{         //errors     }] }]

你可以很好地包含 Id 或根据 API 需要确定的任何标识,这有助于理解响应主体数组中每个对象到请求有效负载的映射。


注意:微服务应该对每个请求允许的资源数量提出并实施限制。

注意:如果所有单个资源请求都返回相同的状态代码,则请求作为一个整体应该返回该状态代码。


版本控制


服务的主要版本是v#格式的 url 的一部分,例如v1等。这允许部署新的主要版本而不会破坏现有的服务使用者。这也有助于实现零停机部署的目标。


每当对现有服务的消费者引入重大更改时,都需要一个主要版本。非破坏性更改(例如添加其它字段或功能)不会触发主要版本增量,应遵循https://semver.org/。可以通过调用本文档的运行状况和版本部分中概述的版本端点来检索服务的当前完整版本。


如果服务不小心将破坏性更改推送到现有版本,则它们必须立即回滚该更改,因为该服务违反了其版本合同。服务应维护自动化测试,以验证它们不会因更新而意外破坏现有版本的使用者。


只有在存在必须立即修补的关键安全问题时,才允许对生产中的消费者对现有版本进行重大更改。


API错误处理实践总结


  • 始终返回相关的HTTP 状态代码

  • 对于由于客户端输入导致的错误,返回 4xx 状态代码

  • 对于由于服务器实现或当前服务器状态导致的错误,返回 5xx 状态代码

  • 如果适用(除了 HEAD 请求),始终在响应正文中包含JSON 错误表示

  • 错误应该包含消息之外的相关信息,以便机器尽可能地解析

  • 如果有一个适用于多个字段的通用代码,则响应中应该有另一个字段指定它适用于哪个字段

  • 保持响应正文的描述性。连同错误消息一起,描述客户端可以采取的任何操作来纠正错误(如果适用)

  • 不要暴露内部技术。永远不应在错误响应中返回诸如堆栈跟踪或系统错误消息之类的详细信息。事实上,Service 不应该在错误中返回任何我们没有专门编写或处理的内容。任何未处理的错误都应作为“意外错误”返回

  • 如果纠正或调试错误的信息可作为单独的人类可读文档提供,请在响应正文中包含指向该文档的链接(可能与面向公众的 API 更相关)

  • 错误代码是对错误进行进一步分类的好方法,它还使消费者能够以更有意义的方式处理错误。


响应消息体


错误描述对象应包括:

  • 错误代码 — 错误类型标识符(请检查下面的错误代码部分)

  • 消息 — 包含有关错误以及如何修复错误的信息的描述

  • Body 应该有一个错误描述对象列表——有时,我们可能想要为给定的请求返回多个错误。因此,我们应该始终在响应正文中返回错误列表,以避免在这种情况下出现并发症。

  • 如果需要,服务只能本地化消息字段。所有密钥和错误代码应保持原样。


错误代码


  • 错误代码是错误类型的简短、人类可读的密钥(主要是标识错误的简短字母数字值)。它不应出现变化,且应限于有限的一组值;

  • 有一些通用错误代码可用于多个服务。例如RequiredFieldMissing、FieldInvalid、NoRecordFound 等。这样的错误代码列表会不断增加,因此它应该是一些公共库的一部分,需正确记录;

  • 如果出现多个错误,只能将具有相同 Http 状态码的错误组合在一起。例如,错误描述不能在同一响应中同时包含 InvalidField 和 NoRecordFound 这样的错误,因为它们分别代表状态 400 和 404;

  • 有时,应用程序可能需要一个非常特定于该应用程序的错误代码。因此,除了常见的错误代码之外,应用程序还可以使用自己的错误代码。


POST 请求与错误响应实例


# RequestPOST /reporting/v1/addresses HTTP/1.1Host: api.itsupport247.netContent-Type: application/jsonAuthorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
{"name":"Demo Address in NOIDA ","addressCode":"Demo"}
# Response example 1HTTP/1.1 400 Bad RequestContent-Type: application/jsonLocation: https://api.itsupport247.net/v1/addresses/Demo
{ "errors": [{ "code": "RequiredFieldMissing", "field" : "address", "message": "address is a required field.", "info": "https://doccenter.itsupport247.net/addresses/creating" }, { "code": "FieldInvalid", "field" : "addressCode", "message": "addressCode needs to be of UUID format."}] }
# Response example 2HTTP/1.1 400 Bad RequestContent-Type: application/jsonLocation: https://api.itsupport247.net/v1/addresses/Demo
{ "errors": [{ "code" : "InvalidField", "field" : "name", "message": "Maximum 10 characters are allowed for name"}] }

参考对象


参考对象允许在主实体请求和响应中嵌套子实体。这可以显着减少 UI 或其他消费者必须发出的请求数量。


  • 子参考对象应该只包含最有可能需要的最少数量字段

  • 一对一的关系更容易包含,而一对多的关系则面临挑战

  • 必须判断哪些子实体应该被包括在内,哪些不应该被包括在内。用户尤其是 UI,每次调用主实体时都必须不断地进行多次调用,这表明它应该是一个引用对象

  • 虽然对包含的嵌套数量没有硬性规定,但在前 2-3 级之后,引用对象可能会影响性能,不应使用

  • 对于更复杂/深度级别的后代实体,可以考虑统一实体服务/图形层仅在请求时允许这些实体。

  • 参考对象的名称不必与子实体名称完全匹配。例如,客户模型可能有 DefaultAddress、ShippingAddress、BillingAddress 等,即使这三个都是相同的 Address 实体。


让我们假设一个示例实体 Order 有两个子实体,Customer 和 Order Status。有效API信息格式如下所示:

{    "order":{       "id":12,       "customerID":8,       "status":{          "id":63,          "name":"Active"       }    } }
  • 由于 Customer 在 Order Service 域之外,因此它始终应是 ID 而不是引用对象;

  • oredr 状态引用对象只包含最少的字段,而不是所有字段,例如 createdBy/createdDate 等;

  • 一种常见的模式是对实体执行 GET,修改某些字段,然后执行 PUT 以更新这些字段。端点应接受其他参考对象字段,例如名称或说明,但应忽略非 ID 字段,并且不要尝试实际更新字段或验证其准确性;

  • 对于 PATCH 操作,尝试通过对父实体的补丁更改非 ID 字段应该会引发错误,因为补丁应该应用于子实体端点本身。


条件过滤


过滤背后的基本思想是限制 GET 调用,以减少返回到需要的子集的结果数量。因此,它不需要为每个节点实现,只应考虑具有这些用例的实体。根据用例可以使用两种不同的实现。


1)简单过滤

对于需要以基本方式限制端点的简单过滤用例。


2)语法

将要过滤的字段作为查询字符串参数传入。基本格式类似于:

.../contacts?firstName=example

其中 firstName 是要过滤的字段,而“Example”是要与之比较的字段。

这种格式比较简单,就是fieldName=value的简单语法。如果要过滤多个字段,可以为不同的字段设置多个查询参数。


列出一些注意事项:

  • 字段名称区分大小写。如果传递了无效的大小写,将被忽略;

  • 值不需要被任何东西包含;

  • 时间戳应以标准ISO8601 格式完成;

  • 布尔值应作为真或假传入;

  • 可以使用圆点表示法过滤嵌套字段;

  • 如果有用例,则仅允许对嵌套字段进行过滤;

  • 如果您希望过滤器允许传入多个值,则可以传入逗号分隔列表

  • swagger 应该为查询字符串参数声明此功能

  • 字段名称仍照常使用


例如如下URI:

.../orders?summary=Test&closedFlag=false.../customers?address.country=China.../contacts?createdAt=2020-11-01T12:00:00.000Z.../address?id=1,2,3

Swagger 文档


根据OpenAPI 3.0 规范,每个受支持的查询字符串都应以 YAML 格式作为查询参数记录在 swagger 中。


复杂过滤


对于需要更灵活过滤的用例,例如基本相等性检查以外的条件,可以扩展替代语法以允许更强大的过滤。


语法


过滤应作为查询字符串参数“过滤器”传递。基本格式类似:

.../contacts?filterName="example"

其中 firstName 是要过滤的字段,而“Example”是要与之比较的字段。


一些细节


REST 语法松散,这里基于Google Api来描述。这使我们能够在必要时支持更高级的节点条件过滤,在它的基础上,节点支持以下语法:

filter=比较{“And”比较运算符}
比较运算符:字段名称 = 值

这表示在基本层,可以将等式运算符链接在一起。

一些详细注意事项如下:


  • 可选择支持“NOT”和“OR”逻辑运算符

  • 可以选择支持“<=”、“<”、“>=”、“>”、“!=”、“:”比较运算符

  • “:”运算符作为一个子字符串运算符。检查传入的字符串是否是字段的子字符串

  • 在某些时候,有必要支持括号以进行优先分组

  • 嵌套字段应在嵌套之间使用句号

  • value 应该被字符串和枚举的双引号括起来

  • 对于数字和布尔值,值不需要被任何东西包围

  • 布尔值可作为真或假传入

  • 时间戳应采用标准ISO8601 格式并用引号括起来。(如:“2021–11–01T12:00:00.000Z”)


以下举一些实例来说明:


.../orders?filter=summary="Test" AND closedFlag=false.../customers?filter=address.country="cn".../contacts?filter=createdAt="2021-01-01T12:00:00.000Z".../contacts?filter=emailAddress:"@21cto.com".../customers?filter=inactiveFlag=false AND NOT address.country="CN".../orders?filter=(summary:"Critical" OR summary:"Emergency") AND (status.Name="New" OR status.name="pending")

Swagger 文档

根据OpenAPI 3.0 规范,过滤器应作为查询参数记录在 swagger 中。说明应包括支持哪些字段和运算符:


以下实例:

参数:
-名称:Filter
:查询
架构:
类型:String
描述:|
允许过滤结果集
允许过滤的字段:名称、关闭标志
允许的操作符:=


分页


分页应用于横向大型数据集并通过多次调用获取所有结果。它不需要为每个端点实现,只应考虑具有超过一页数据的潜力的实体。


虽然它们有多种类型的分页,但使用的默认值应该是前进分页。它也称为游标分页。它是获取大型数据集的最有效方法。


前向分页背后的基本思想是:

  • 消费者拨打电话请求前 x 条记录。

  • 服务器处理请求并说明最后提供的记录(通常以指向下一页的响应头的形式)

  • 消费者然后使用响应头发出另一个请求以获取下 x 条记录

  • 重复步骤 2 和 3,直到检索到所有记录。


这对服务器来说很容易执行,因为它可以将留下的记录转换为查询中的过滤器。但是,它确实会导致一些排序限制。


请求详细信息


分页请求是针对使用一些查询字符串参数集合的 Get 请求:

  • limit:这是希望在每个页面中获取的记录数。如果未指定,则应假定为默认值。

  • cursor:这是处理的最后一条记录。它告诉服务器下次从哪里开始。如果没有传入,则假定要求的是第一页

  • sortBy:这会告诉服务器要根据哪个字段进行排序。没有必要实现这一点,如果需要,可以始终按 id 或创建日期排序。


排序注意事项


如果选择允许用户指定 sortBy 字段,请注意该字段(或字段集)必须是唯一的。否则服务器将不知道它停到了哪个记录。


处理此问题的最简单方法是将最后一个排序字段设为记录的 id,因为它们始终是唯一的。选择排序的任何字段都需要包含在游标中。


响应详情


响应应该像正常一样返回结果,限制为在按 sortBy 字段排序的传入游标之后开始的限制指示的记录数。


除此之外,除非没有其他记录,否则唯一应该包含的是指向下一页的链接标题。


  • 链接标头应遵循RFC 5988

  • 只应包括下一个关系

  • 链接应该反映最初发送的内容,除了添加或更新游标查询字符串以反映返回的最后一条记录。


一些例子


场景:微服务A需要从微服务B获取一个客户的所有订单,该客户有数千个客户,因此需要对数据进行分页。


# Microservice A Request for first 1000 customersGET /customer-service/v1/orders?limit=1000 HTTP/1.1Host: api.customerService.net
# Response# 返回链接头中的下一页链接。游标等于最后一条记录的id
HTTP/1.1 200 OKContent-Type: application/jsonLink: ; rel="next"
[{"id":"abfa6d90-eb97-432e-ba38-04db482d9cef","name":"客户样本"},{"id":"acca6d11-ab12-432e-ce33-16hb123d9by3","name":"客户示例"},...(这里还有 997 个客户){"id":"e8fa6d90-eb97-432e-ba38-04db482d9cef","name":"客户测试"}]# Microservice A Request then uses the link header to request the next 1000 customersGET /customer-service/v1/customers?limit=1000&cursor=e8fa6d90-eb97-432e-ba38-04db482d9cef HTTP/1.1Host: api.customerService.net
# Response# Returns the next page link in the Link header. The cursor equals the id of the last record
HTTP/1.1 200 OKContent-Type: application/jsonLink: ; rel="next"
[{"id":"fafa6d90-ls67-d12e-bb12-04ac48219tet","name":"客户样品"},{"id":"fec124a9-ab12-432e-c2s3-23hb123ac4yz","name":"客户示例"},...(这里还有 997 个客户){"id":"j8fa61zw-ew9z-432e-ba42-0adb4s2dabc3","name":"客户测试"}]# Microservice A Then continues to keep getting the next page until it has gotten all the customers


字段映射


为客户端提供一种选择在响应中返回哪些字段的方法(资源的表示)。
这有助于减少响应时间和有效负载大小。


如何实现:
使用“fields”查询字符串参数指定在响应中返回哪些字段。此参数将接受表示所需字段的以逗号分隔的字符串列表。

最佳实践

  • 使用的字段名称应区分大小写,如果大小写不匹配,服务器应抛出适当的错误。

  • 使用无效字段名称应由服务器作为适当的错误抛出。

默认字段

默认字段是一个可选概念。当资源负载包含大量字段且并非所有字段都与客户端相关时,应使用它以帮助减少默认负载大小


一些例子


场景:地址是一个“昂贵”的字段,我们API不想返回,除非消费者明确要求。

客户资源字段:
id => 默认
名称 => 默认
电话 => 默认
地址 => 非默认

这里我们只做一个普通的调用:

# 请求GET /api/v1/customers# 响应(不返回地址,因为它不是默认的)[   { "id" : 1, "name" : "Customer 1", "phone" : "111-567-1234" },   { "id" : 2, "姓名”:“客户 2”,“电话”:“222-767-1235”},  {“id”:3,“姓名”:“客户 3”,“电话”:“333-978-1678”} ]

这里我们会查询具体的字段:

# 请求GET /api/v1/customers?fields=id,name,address# 响应(返回明确要求的地址,不返回未要求的电话)[  { "id" : 1, "name" : "Customer 1", "address" : {"city" : " c1", "country" : "Country1"}},   { "id" : 2, "name" : "Customer 2", "address" : {"city" : "c2", "country" : "Country2"} },   { "id" : 3, "name" : "Customer 3", "address" : {"city": "c3", "country" : "Country3"}} ]

说明:

  • 一旦一个字段被标记为默认值,它就不能被标记为非默认值,因为这对客户端来说可能是一个破坏性的变化。

  • 在 swagger 中,应该记录默认情况下不会返回哪些字段

  • 如果在这些情况下需要较小的响应大小,除了 GET 之外,还可以选择为 POST、PATCH 和 PUT 实现


健康检查API


  • 健康检查 API 必须是微服务组件中单独 REST 服务调用。

  • 这些调用应该是无版本的,并且没有 uri 的客户部分

  • API 必须返回组件的运行状态及其连接到它所依赖的下游组件的能力。例如数据库、消息队列等

  • 它不应该调用下游微服务健康节点,这可能会导致连锁故障

  • 可以调用下游微服务的 /version 版本端点来检查基本连通性

  • 可以扩展高级健康检查 API 返回性能信息,例如连接时间

  • 结果必须返回作为带有 JSON 数据的 HTTP 状态代码

  • Health API 响应应包含以下详细信息:

    通用 — ServiceName、时间戳、版本、状态和连接特定 — ConnectionType、ConnectionStatus、Timestamp、ListenUrl 等。

  • 一个例子 -

    { “timeStampUTC”: “2020–10–29T07:39:47.3224602Z”, “serviceName”: “入站订单服务”, “serviceProvider”: “Google Alphabet LLC”, “serviceVersion”: “1.0.0”, “type” ”:“健康”,“状态”:“正在运行”,“lastStartTimeUTC”:“2020-10-29T07:39:39.9232975Z”,“outboundConnectionStatus”:[{ “timeStampUTC”:“2020-10-29T07:39:47.3224608Z”,“type”:“OutboundConnectionStatus”,“name”:“Inbound-Order-Service-Database-Status”,“connectionType”:“Cassandra”,“connectionURLs”:[“cassandra:9042”],“connectionStatus” ”:“活动”},{“timeStampUTC”:“2020–10–29T07:39:47.3224608Z”,“类型”:“OutboundConnectionStatus”,“名称”:“入站订单服务-SQS-状态”,“ connectionType”: “SQS”, “connectionURLs”: [“http://sqs:9324”], “connectionStatus”: “Active” }] }

版本API


  • 与健康API类似,版本 API 必须是微服务组件中单独 REST 服务调用。

  • 结果必须作为带有 JSON 数据的 HTTP 状态代码返回。

  • API 应返回以下详细信息:

    服务名称、时间戳、当前版本、支持的版本、repo 链接、内部版本号等。

  • 例子 -

    { “timeStampUTC”: “2020–10–29T07:59:55.0856569Z”, “serviceName”: “入站订单服务”, “serviceProvider”: “Google Alphabet LLC”, “serviceVersion”: “1.0.0”, “type” ”:“版本”,“buildCommitSHA”:“60a421afc030ef8304b4a9498d664423a47fdfc”,“存储库”:“https://github.com/saas/inbound-order-service”,“supportedAPIVersions”:[“v1”],“buildNumber”:“1.0.0–0” }


HTTP 标头


  • HTTP 标头可用于传递与请求或响应相关的任何元数据。它必须只包含元数据,不能包含任何与字段、参数等内容;

  • 标头中的交易 ID :'X-Request-Id'
    此事务 ID 用于关联客户端和服务器之间的 HTTP 请求;

  • 因此,每个服务都应该从请求中读取 TransactionID 标头,如果它为空,则该服务应该创建一个新的。此事务 ID 必须转发到任何进一步的 API 调用,并应在日志记录中使用;

  • Content-Type 

    服务应始终在响应标头中返回一个“Content-Type”,它表示响应正文的媒体类型。


结论

一起总结一下Restful API 的开发与实践规则:

  • 使用名词不要使用动词

  • GET 方法和查询参数不应改变状态

  • 使用复数名词

  • 使用子资源建立关系

  • 使用 HTTP 标头进行序列化格式

  • 使用 HATEOAS

  • 为集合提供过滤、排序、字段选择和分页

  • 要有自己的版本API

  • 使用 HTTP 状态码处理错误

  • 允许API 覆盖 HTTP 方法


开发人员在设计 REST API 时需要花一些精力,但这是必要的,因为 API 可以使服务易于使用或变得复杂。


本文第一版本完成,有错漏之处欢迎大家指出,我们不断更新。


作者:万能的大雄


评论