大多数现代的Web应用程序都公开了客户端能够用来与应用程序交互的API。精心设计的Web API应该旨在支持:
数据库
平台独立性。不管内部如何实现API,任何客户端都应该可以调用API。这须要使用标准协议,并具备一种机制,使客户端和Web服务能够就要交换的数据格式达成一致。json
服务演进。Web API应该可以独立于客户端应用程序进行演化和添加功能。随着API的发展,现有的客户端应用程序应继续运行而无需修改。全部功能都应该是可发现的,以便客户端应用程序能够充分使用它。后端
本文介绍了设计Web API时应考虑的问题。api
在2000年,罗伊·菲尔丁(Roy Fielding)提出了表明性状态转移(REST)做为设计Web服务的体系结构方法。REST是一种用于构建基于超媒体的分布式系统的体系结构样式。REST独立于任何底层协议,不必定与HTTP绑定。可是,大多数常见的REST实现使用HTTP做为应用程序协议,而且本指南重点介绍为HTTP设计REST API。缓存
REST相对于HTTP的主要优点在于它使用开放标准,而且不会将API或客户端应用程序的实现绑定到任何特定的实现。例如,REST Web服务能够用ASP.NET编写,而且客户端应用程序可使用能够生成HTTP请求并解析HTTP响应的任何语言或工具集。bash
如下是使用HTTP的RESTful API的一些主要设计原则:服务器
REST API是围绕网络
资源具备数据结构
客户端经过交换资源架构
REST API使用统一的接口,这有助于使客户端和服务实现脱钩。对于基于HTTP构建的REST API,统一接口包括使用标准HTTP动词对资源执行操做。最多见的操做是GET,POST,PUT,PATCH和DELETE。
REST API使用无状态请求模型。HTTP请求应该是独立的,而且能够以任何顺序发生,所以在请求之间保留瞬态信息是不可行的。信息存储的惟一位置是资源自己,每一个请求都应该是原子操做。此约束使Web服务具备高度可伸缩性,由于不须要在客户端和特定服务器之间保留任何亲缘关系。任何服务器均可以处理来自任何客户端的任何请求。也就是说,其余因素可能会限制可伸缩性。例如,许多Web服务写入后端数据存储,这可能很难扩展。
REST API由表示形式中包含的超媒体连接驱动。例如,如下显示了订单的JSON表示形式。它包含获取或更新与订单关联的客户的连接。
2008年,伦纳德·理查森(Leonard Richardson)为Web API 提出了如下成熟度模型:
级别0:定义一个URI,而且全部操做都是对此URI的POST请求。
级别1:为单个资源建立单独的URI。
级别2:使用HTTP方法定义对资源的操做。
级别3:使用超媒体(HATEOAS,以下所述)。
根据Fielding的定义,级别3对应于真正的RESTful API。实际上,许多已发布的Web API都属于2级左右。
着重于Web API公开的业务实体。例如,在电子商务系统中,主要实体多是客户和订单。建立订单能够经过发送包含订单信息的HTTP POST请求来实现。HTTP响应指示订单是否成功下达。若是可能,资源URI应基于名词(资源)而不是动词(对资源的操做)。
https://www.abc.com/orders // Goodhttps://www.abc.com/create-order // Avoid复制代码
资源没必要基于单个物理数据项。例如,订单资源可能在内部实现为关系数据库中的多个表,但做为单个实体呈现给客户。避免建立仅反映数据库内部结构的API。REST的目的是为实体以及应用程序能够在这些实体上执行的操做建模。客户不该接触内部实现。
实体一般被组合到集合中(订单,客户)。集合是与集合中项目无关的资源,而且应具备本身的URI。例如,如下URI可能表明订单的集合:
https://www.abc.com/orders
向集合URI发送HTTP GET请求可检索集合中的项目列表。集合中的每一个项目都有本身的惟一URI。对商品URI的HTTP GET请求返回该商品的详细信息。
在URI中采用一致的命名约定。一般,对引用集合的URI使用复数名词会有所帮助。将集合和项目的URI组织到层次结构中是一个好习惯。例如,/customers
是通往客户集合/customers/5
的路径,是通往ID等于5的顾客的路径。这种方法有助于保持Web API的直观性。另外,许多Web API框架均可以基于参数化URI路径来路由请求,所以您能够为path定义路由/customers/{id}
。
还请考虑不一样类型的资源之间的关系以及如何暴露这些关联。例如,/customers/5/orders
可能表明客户5的全部订单。您也能够朝另外一个方向前进,并使用URI之类的URI表示从订单到客户的关联/orders/99/customer
。可是,将模型扩展得太远可能难以实施。更好的解决方案是在HTTP响应消息的正文中提供指向关联资源的可导航连接。
在更复杂的系统中,提供URI使客户端可以浏览多个级别的关系(例如)可能很诱人/customers/1/orders/99/products
。可是,若是未来资源之间的关系发生变化,则这种复杂性级别可能难以维护,而且很难保持灵活性。相反,请尝试使URI保持相对简单。一旦应用程序引用了资源,就应该可使用该引用来查找与该资源有关的项目。能够将前面的查询替换为URI,/customers/1/orders
以查找客户1的全部订单,而后/orders/99/products
查找此订单中的产品。
另外一个因素是,全部Web请求都会对Web服务器施加负载。请求越多,负载越大。所以,请尝试避免使用暴露大量小资源的“聊天” Web API。这样的API可能要求客户端应用程序发送多个请求以查找其所需的全部数据。取而代之的是,您可能想对数据进行非规范化并将相关信息合并为更大的资源,这些资源能够经过单个请求检索。可是,您须要在此方法与获取客户端不须要的数据的开销之间取得平衡。检索大对象可能会增长请求的延迟,并致使额外的带宽成本。
避免在Web API和基础数据源之间引入依赖关系。例如,若是您的数据存储在关系数据库中,则Web API不须要将每一个表都显示为资源的集合。实际上,这多是一个糟糕的设计。相反,能够将Web API视为数据库的抽象。若有必要,请在数据库和Web API之间引入一个映射层。这样,客户端应用程序就不会与基础数据库方案的更改保持隔离。
最后,可能没法将Web API实施的每一个操做映射到特定资源。您能够经过调用功能的HTTP请求来处理此类
HTTP协议定义了许多将语义分配给请求的方法。大多数RESTful Web API使用的常见HTTP方法是:
GET以指定的URI检索资源的表示形式。响应消息的正文包含所请求资源的详细信息。
POST在指定的URI处建立一个新资源。请求消息的正文提供了新资源的详细信息。请注意,POST也能够用于触发实际上并不建立资源的操做。
PUT能够建立或替换指定URI处的资源。请求消息的正文指定要建立或更新的资源。
PATCH执行资源的部分更新。请求主体指定要应用于资源的一组更改。
DELETE删除指定URI处的资源。
特定请求的效果应取决于资源是集合仍是单个项目。下表使用电子商务示例总结了大多数RESTful实现所采用的通用约定。并不是全部这些请求均可以实现-这取决于特定的方案。
资源资源 | 开机自检 | 获得 | 放 | 删除 |
---|---|---|---|---|
/顾客 | 创建新客户 | 检索全部客户 | 批量更新客户 | 删除全部 客户 |
/客户/ 1 | 错误 | 检索客户1的详细信息 | 更新客户1的详细信息(若是存在) | 删除客 户1 |
/ customers / 1 /订单 | 为客户1建立新订单 | 检索客户1的全部订单 | 批量更新客户1的订单 | 删除客户1的全部订单 |
POST,PUT和PATCH之间的差别可能使人困惑。
POST请求建立资源。服务器为新资源分配一个URI,并将该URI返回给客户端。在REST模型中,您常常将POST请求应用于集合。新资源将添加到集合中。POST请求也能够用于提交数据以处理现有资源,而无需建立任何新资源。
PUT请求建立资源
PATCH请求对现有资源执行
PUT请求必须是幂等的。若是客户端屡次提交相同的PUT请求,则结果应始终相同(将使用相同的值修改相同的资源)。POST和PATCH请求不保证是幂等的。
本节描述了设计符合HTTP规范的API的一些典型注意事项。可是,它没有涵盖全部可能的细节或场景。若有疑问,请查阅HTTP规范。
如前所述,客户端和服务器交换资源的表示形式。例如,在POST请求中,请求主体包含要建立的资源的表示形式。在GET请求中,响应主体包含获取的资源的表示形式。
在HTTP协议中,经过使用
请求或响应中的Content-Type标头指定表示的格式。这是一个包含JSON数据的POST请求示例:
POSThttps://adventure-works.com/orders HTTP/1.1
Content-Type: application/json; charset=utf-8
Content-Length: 57
{"Id":1,"Name":"Gizmo","Category":"Widgets","Price":1.99}
若是服务器不支持媒体类型,则应返回HTTP状态码415(不支持的媒体类型)。
客户端请求能够包含一个Accept标头,该标头包含客户端将在响应消息中从服务器接受的媒体类型列表。例如:
GEThttps://adventure-works.com/orders/2 HTTP/1.1 Accept: application/json
若是服务器没法匹配列出的任何媒体类型,则服务器应返回HTTP状态代码406(不可接受)。
成功的GET方法一般返回HTTP状态代码200(肯定)。若是找不到资源,则该方法应返回404(未找到)。
若是POST方法建立新资源,它将返回HTTP状态代码201(已建立)。新资源的URI包含在响应的Location标头中。响应主体包含资源的表示形式。
若是该方法进行了一些处理但未建立新资源,则该方法能够返回HTTP状态代码200,并将操做结果包括在响应主体中。或者,若是没有要返回的结果,则该方法能够返回没有响应正文的HTTP状态代码204(无内容)。
若是客户端将无效数据放入请求中,则服务器应返回HTTP状态代码400(错误请求)。响应主体能够包含有关错误的其余信息,也能够包含提供更多详细信息的URI连接。
PUT方法
若是PUT方法建立新资源,则与POST方法同样,它返回HTTP状态代码201(已建立)。若是该方法更新了现有资源,则返回200(肯定)或204(无内容)。在某些状况下,可能没法更新现有资源。在这种状况下,请考虑返回HTTP状态代码409(冲突)。
考虑实现批量HTTP PUT操做,该操做能够批量更新集合中的多个资源。PUT请求应指定集合的URI,请求主体应指定要修改的资源的详细信息。这种方法能够帮助减小聊天状况并提升性能。
若是删除操做成功,则Web服务器应使用HTTP状态代码204进行响应,指示该过程已成功处理,可是响应主体不包含其余信息。若是资源不存在,则Web服务器能够返回HTTP 404(未找到)。
有时POST,PUT,PATCH或DELETE操做可能须要一些处理才能完成。若是在发送响应到客户端以前等待完成,则可能会致使没法接受的延迟。若是是这样,请考虑使操做异步。返回HTTP状态码202(已接受)以指示请求已接受处理,但未完成。
您应该公开一个返回异步请求状态的端点,以便客户端能够经过轮询状态端点来监视状态。在202响应的Location标头中包含状态终结点的URI。例如:
HTTP/1.1 202 Accepted
Location: /api/status/12345
若是客户端将GET请求发送到此端点,则响应应包含请求的当前状态。可选地,它还能够包括估计的完成时间或取消操做的连接。
HTTP/1.1 200 OK
Content-Type: application/json
{
"status":"In progress", "link": { "rel":"cancel", "method":"delete", "href":"/api/status/12345" } }
若是异步操做建立了新资源,则该操做完成后,状态端点应返回状态代码303。在303响
应中,包括一个Location标头,该标头提供了新资源的URI:
HTTP/1.1 303 See Other
Location: /api/orders/12345
当仅须要一部分信息时,经过单个URI公开资源集合可能致使应用程序获取大量数据。例如,假设客户端应用程序须要查找成本超过特定值的全部订单。它可能会从
相反,API能够容许在URI的查询字符串中传递过滤器,例如
minCost
查询字符串中的参数,并在服务器端返回过滤后的结果。
收集资源上的GET请求可能会返回大量项目。您应该设计一个Web API来限制任何单个请求返回的数据量。考虑支持查询字符串,这些字符串指定要检索的最大项目数以及集合中的起始偏移量。例如:
/orders?limit=25&offset=50
还应考虑对返回的项目数施加上限,以帮助防止拒绝服务攻击。为了帮助客户端应用程序,返回分页数据的GET请求还应该包括某种形式的元数据,以指示集合中可用资源的总数。
经过提供将字段名做为值的排序参数,例如
若是每一个项目包含大量数据,则能够扩展此方法以限制为每一个项目返回的字段。例如,您可使用查询字符串参数,该参数接受以逗号分隔的字段列表,例如
在查询字符串中为全部可选参数提供有意义的默认值。例如,若是实现分页,则将参数设置limit
为10,将offset
参数设置为0,若是实现排序,则将sort参数设置为资源的键,fields
若是支持投影,则将参数设置为资源中的全部字段。
资源可能包含较大的二进制字段,例如文件或图像。为了克服由不可靠和间歇性链接引发的问题并改善响应时间,请考虑使此类资源可以分块检索。为此,Web API应该支持用于大资源的GET请求的Accept-Ranges标头。此标头表示GET操做支持部分请求。客户端应用程序能够提交GET请求,该请求返回指定为字节范围的资源子集。
另外,请考虑为这些资源实现HTTP HEAD请求。HEAD请求与GET请求类似,不一样之处在于,它仅返回描述资源的HTTP标头,且消息正文为空。客户端应用程序能够发出HEAD请求,以肯定是否经过使用部分GET请求来获取资源。例如:
HEADhttps://adventure-works.com/products/10?fields=productImage HTTP/1.1
这是示例响应消息:
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 4580
Content-Length标头提供资源的总大小,Accept-Ranges标头指示相应的GET操做支持部分结果。客户端应用程序可使用此信息以较小的块来检索图像。第一个请求经过使用Range标头获取前2500个字节:
HTTP/1.1 303 See Other
Location: /api/orders/12345
当仅须要一部分信息时,经过单个URI公开资源集合可能致使应用程序获取大量数据。例如,假设客户端应用程序须要查找成本超过特定值的全部订单。它可能会从
相反,API能够容许在URI的查询字符串中传递过滤器,例如
minCost
查询字符串中的参数,并在服务器端返回过滤后的结果。
收集资源上的GET请求可能会返回大量项目。您应该设计一个Web API来限制任何单个请求返回的数据量。考虑支持查询字符串,这些字符串指定要检索的最大项目数以及集合中的起始偏移量。例如:
/orders?limit=25&offset=50
还应考虑对返回的项目数施加上限,以帮助防止拒绝服务攻击。为了帮助客户端应用程序,返回分页数据的GET请求还应该包括某种形式的元数据,以指示集合中可用资源的总数。
经过提供将字段名做为值的排序参数,例如
若是每一个项目包含大量数据,则能够扩展此方法以限制为每一个项目返回的字段。例如,您可使用查询字符串参数,该参数接受以逗号分隔的字段列表,例如
在查询字符串中为全部可选参数提供有意义的默认值。例如,若是实现分页,则将参数设置limit
为10,将offset
参数设置为0,若是实现排序,则将sort参数设置为资源的键,fields
若是支持投影,则将参数设置为资源中的全部字段。
资源可能包含较大的二进制字段,例如文件或图像。为了克服由不可靠和间歇性链接引发的问题并改善响应时间,请考虑使此类资源可以分块检索。为此,Web API应该支持用于大资源的GET请求的Accept-Ranges标头。此标头表示GET操做支持部分请求。客户端应用程序能够提交GET请求,该请求返回指定为字节范围的资源子集。
另外,请考虑为这些资源实现HTTP HEAD请求。HEAD请求与GET请求类似,不一样之处在于,它仅返回描述资源的HTTP标头,且消息正文为空。客户端应用程序能够发出HEAD请求,以肯定是否经过使用部分GET请求来获取资源。例如:
HEADhttps://adventure-works.com/products/10?fields=productImage HTTP/1.1
这是示例响应消息:
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 4580
Content-Length标头提供资源的总大小,Accept-Ranges标头指示相应的GET操做支持部分结果。客户端应用程序可使用此信息以较小的块来检索图像。第一个请求经过使用Range标头获取前2500个字节:
GEThttps://adventure-works.com/products/10?fields=productImage HTTP/1.1
Range: bytes=0-2499
响应消息经过返回HTTP状态代码206指示这是部分响应。Content-Length标头指定了消息正文中返回的实际字节数(不是资源的大小),而Content-Range标头指示了哪一个响应。这是资源的一部分(4580中的字节0-2499):
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 2500
Content-Range: bytes 0-2499/4580
来自客户端应用程序的后续请求能够检索资源的其他部分。
REST背后的主要动机之一是,无需事先了解URI方案,就应该能够浏览整个资源集。每一个HTTP GET请求应经过响应中包含的超连接返回查找与请求的对象直接相关的资源所必需的信息,而且还应向其提供描述这些资源中的每个可用操做的信息。此原理称为HATEOAS,或称为应用程序状态引擎的超文本。该系统其实是一个有限状态机,对每一个请求的响应都包含从一种状态转移到另外一种状态所需的信息。不须要其余信息。
Web API保持静态的可能性很小。随着业务需求的变化,可能会添加新的资源集合,资源之间的关系可能会更改,而且资源中的数据结构可能会被修改。尽管更新Web API以处理新的或不一样的要求是一个相对简单的过程,可是您必须考虑此类更改将对使用Web API的客户端应用程序产生的影响。问题在于,尽管设计和实现Web API的开发人员能够彻底控制该API,可是开发人员对客户端应用程序的控制程度不一样,该客户端应用程序能够由远程运行的第三方组织构建。
经过版本控制,Web API能够指示其公开的功能和资源,而且客户端应用程序能够提交针对功能或资源的特定版本的请求。如下各节描述了几种不一样的方法,每种方法都有其自身的优势和取舍。
这是最简单的方法,对于某些内部API多是可接受的。重大更改能够表示为新资源或新连接。向现有资源添加内容可能不会带来重大变化,由于不但愿看到此内容的客户端应用程序将忽略它。
例如,到URI的请求https://www.abc.com/customers/3
应该返回单个客户的含有细节id
,name
以及address
由所述客户端应用程序预期字段:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8 {"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}
若是将该DateCreated
字段添加到客户资源的架构,则响应将以下所示:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":"1 Microsoft Way Redmond WA 98053"}
若是现有的客户端应用程序可以忽略没法识别的字段,则它们可能会继续正常运行,而新的客户端应用程序能够设计为处理此新字段。可是,若是对资源的架构进行了更根本的更改(例如,删除或重命名字段),或者资源之间的关系发生了更改,则这些更改可能构成重大更改,从而阻止现有客户端应用程序正常运行。在这些状况下,您应该考虑使用如下方法之一。
每次您修改Web API或更改资源架构时,您都会为每一个资源的URI添加一个版本号。先前存在的URI应该继续像之前同样操做,返回符合其原始架构的资源。
延伸的前面的例子,若是该address
字段被重组为包含地址的每一个组成部分(如子场streetAddress
,city
,state
,和zipCode
),这个版本的资源的可经过URI暴露包含一个版本号,如 https://adventure-works.com/v2/customers/3
:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":{"streetAddress":"1 Microsoft Way","city":"Redmond","state":"WA","zipCode":98053}
这种版本控制机制很是简单,可是取决于服务器将请求路由到适当的端点。可是,随着Web API经过屡次迭代成熟,而且服务器必须支持许多不一样的版本,它可能变得笨拙。一样,从纯粹主义者的角度来看,在全部状况下,客户端应用程序都在提取相同的数据(客户3),所以URI不该真正取决于版本。此方案还使HATEOAS的实现复杂化,由于全部连接都须要在其URI中包括版本号。
您可使用附加到HTTP请求的查询字符串中的参数(例如)来指定资源的版本,而不是提供多个URI https://www.abc.com/customers/3?version=2
。若是较旧的客户端应用程序省略了版本参数,则该版本参数应默认为有意义的值,例如1。
这种方法具备语义优点,即始终从相同的URI中检索相同的资源,可是它取决于处理请求的代码以解析查询字符串并发送回适当的HTTP响应。这种方法还遭受与URI版本控制机制相同的实现HATEOAS的复杂性。
您能够实现一个指示资源版本的自定义标头,而不是将版本号附加为查询字符串参数。这种方法要求客户端应用程序将适当的标头添加到任何请求,尽管若是省略了版本标头,则处理客户端请求的代码可使用默认值(版本1)。如下示例使用名为
版本1:
GEThttps://www.abc.com/customers/3 HTTP/1.1
Custom-Header: api-version=1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}
版本2:
GEThttps://www.abc.com/customers/3 HTTP/1.1
Custom-Header: api-version=2
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":{"streetAddress":"1 Microsoft Way","city":"Redmond","state":"WA","zipCode":98053}}
与前两种方法同样,实现HATEOAS要求在任何连接中包括适当的自定义标头。
当客户端应用程序将HTTP GET请求发送到Web服务器时,应按照本指南前面所述,指定它可使用Accept标头处理的内容格式。一般,
GEThttps://adventure-works.com/customers/3 HTTP/1.1
Accept: application/vnd.adventure-works.v1+json
处理请求的代码负责处理
的格式)。Web服务器经过使用Content-Type标头确认响应正文中的数据格式:
HTTP/1.1 200 OK
Content-Type: application/vnd.adventure-works.v1+json; charset=utf-8
{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}
若是Accept标头未指定任何已知的媒体类型,则Web服务器能够生成HTTP 406(不可接受)响应消息或返回具备默认媒体类型的消息。
这种方法能够说是最纯粹的版本控制机制,而且很天然地适合HATEOAS,后者能够在资源连接中包括相关数据的MIME类型。
注意: 选择版本控制策略时,还应考虑对性能的影响,尤为是Web服务器上的缓存。URI版本控制和查询字
符串版本控制方案是缓存友好的,由于相同的URI /查询字符串组合每次都引用相同的数据。标头版本控制
和媒体类型版本控制机制一般须要其余逻辑来检查自定义标头或“接 受”标头中的值。在大规模环境中,
许多使用不一样版本的Web API的客户端可能会 在服务器端缓存中致使大量重复数据。若是客户端应用程序
经过实现缓存的代理与Web服务器通讯,而且仅在当前不将请求数据的副本保存在其缓存中的状况下将请求
转发到Web服务器,则此问题可能变得很严重。复制代码
感谢阅读!
喜欢本文的朋友,欢迎关注“isevena”