设计优秀的REST风格API很是困难!API是服务提供方和使用方之间的契约,打破该契约将会给服务端开发人员招来很是大的麻烦,这些麻烦来自于使用API的开发人员,由于对API的改动会致使他们的移动app没法工做。一个好的文档对于解决这些事情能起到事半功倍的做用,可是绝对多数程序员都不喜欢写文档。html
若是想让服务端的价值更好的体现出来,就要好好设计API。经过这些API,你的服务/核心程序将有可能成为其余项目所依赖的平台;目前的大公司:Facebook、Twitter、Google、Github、Amazon、Netflix等等无不依赖API,若是没有精心设计的API对外开发它们的数据,这些公司也就不会像今天这么强大。事实上,整个产业存在的目的就是消费上述平台提供的数据。java
你提供的API越易用,就会有越多人愿意使用它。git
若是在设计API时能遵循本文档提出的原则,那么你设计出的API就能让调用方更容易理解和使用,也能大幅减小调用方对你的抱怨;我已将文档内容按主题分别进行详细描述,读者可选择本身感兴趣的主题,而无需顺序阅读。程序员
本文档中所使用的术语及其含义以下:github
规划API的展现形式可能比你想象的要简单,首先要肯定你的数据是如何设计以及核心程序是如何工做的?在新开发项目中进行API设计会比较容易,若是要对一个已经存在的项目进行修改使之符合REST风格,那么你就须要在抽象方面多下功夫了。[逍遥子笔记:按照RoyThomas Fielding对REST的设计,REST风格更适用于以数据为中心的架构,而非以计算为中心的架构]数据库
有时,集合(Collection)能够表示数据库里的一张表,资源(Resource)表示表中的一行。[逍遥子笔记:这里的比喻感受有些不恰当,数据库的一行对应的是一个资源实体,而非资源!]可是多数状况不是这样简单。事实上,你的API应该是对数据和业务逻辑的“尽量的”抽象。很是重要的一点是:复杂的应用数据将会让第三方开发人员理解和使用起来很是困难,若是你这么作了,他们就不想使用你的API了。[逍遥子笔记:设计API时,参数和返回值的数据不该太复杂,不然开发人员发起调用和处理返回结果时都要处理半天,很是麻烦!]json
有些状况下,服务的数据不能经过API暴露出来。一个常见的例子就是许多API都不容许第三方开发人员建立用户。[逍遥子笔记:设计API时,须要提供什么功能时就提供什么API出来,不要过早、过多暴露没必要要的API接口,咱们开发过程当中一般会遇到这种状况:不管可否用到,先把本身服务的全部功能暴露出来再说,说不定就用上了,到时候就免得再修改了!]api
你确定知道HTTP的GET和POST请求,这是两个经过浏览器访问各类网页时最经常使用的请求。术语POST甚至变成一我的们的经常使用语,即便不知道互联网如何工做的用户也知道能POST信息到朋友的Facebook留言板里。浏览器
你须要了解这里列出的4.5个很是重要的HTTP动做,这里的0.5个是指PATCH,由于它在功能上与PUT很是相似,剩下4个一般被API开发人员两两结合使用[逍遥子笔记:例如GET和POST,PUT和DELETE]。这里是这些动做以及它们对应的数据库调用(我认为大多开发人员更熟悉数据库操做而不是设计API)。[逍遥子笔记:正式基于做者的这个理解,因此本文的不少地方都是用数据库来解释API,其实这个也很是恰当,由于REST架构风格是基于资源的,而数据库也是资源的一种形式。下面这些解释中,括号里的内容就是与该HTTP动词相似的数据库操做]缓存
还有两个不常见的HTTP动做:
一个优秀的API将会充分利用这4.5个HTTP动做让第三方开发人员与本身的数据交互,而且URL中决不包含动做/动词。[逍遥子笔记:URL是对资源描述的抽象,资源的描述必定是名词,若是引入了动词,那这个URL就表示了一个动做,而非一个资源,这样就偏离了REST的设计思想]
一般,GET请求可以被浏览器缓存(并且一般都会这么作),例如,当用户发起第二次POST请求时,缓存的GET请求(依赖于缓存首部)可以加快用户的访问速度。一个HEAD请求基本上就是一个没有返回体的GET请求,所以也能被缓存。
不管你在设计什么系统,也无论你事先作了多么详尽的计划,随着时间的推移和业务的发展,你的程序总会发生变化,数据关系也会发生变化,资源可能会被添加或者删除一些属性。只要软件还在生存期内而且还有人在用它,开发人员就得面对这些问题,对于API设计来讲,尤为如此。[逍遥子笔记:根据RoyThomas Fielding对资源的解释:资源描述和资源实体是分开的,而设计REST API是基于资源描述,当资源实体发生变动时,只要修改资源描述和资源实体的映射,就能保证资源描述不变,进而保证所设计的API不变,全部使用API的第三方程序也不须要作任何修改,所以,REST风格就是用于解耦这种服务端和客户端的关系]
API是一份调用方(Consumer)和服务器之间已达成的契约,更改服务器的API必然会影响其向后兼容性,对契约的破坏将会招导致用方(Consumer)的抱怨,若是你改动很大,他们可能会放弃使用你的服务。为了确保服务器程序可以进化升级,同时可以让使用方感到满意,你须要在引入新版本API的同时继续让旧版本的API正常工做。
注意,若是你只是简单地为API增长一些新特性,例如为资源增长新属性(这些新属性并不是必须的,没有它们资源也能工做),或者新增了端点,那就不须要升级API的版本号,由于这些变化并不会破坏向后的兼容性。固然,你仍是须要更新API设计文档(你和调用方的契约)。[逍遥子笔记:文档对于API来讲过重要了,没有好的文档必然没有好的API,所以API和它的文档必定同步修改,甚至要先修改文档]
过一段时间以后,你能够告诉调用方不建议(deprecate)使用旧版本[逍遥子笔记:就像java里面的depredated注释同样,用于告诉使用方,我不建议你使用它了,过一段时间以后我可能就不支持它了]。不建议使用一个API并不意味着立刻就要关闭它或者下降它的服务质量,而是告诉你的API使用人员他们须要版本升级,旧的版本将在将来一段时间以后被中止服务。[逍遥子笔记:经过过渡期来提醒用户升级API,明确告诉调用方什么API处在过渡期,过渡期内新老版本同时都能工做,但老版本在过渡期以后就会被去掉]
在URL中加入版本号是一个优秀的API设计,固然还有另外一个经常使用的解决办法就是把版本号放在请求首部中[逍遥子笔记:HTTP请求的accept字段],根据多年与第三方开发人员打交道的经验,我能够告诉你把版本号放在URL里要比放在请求的首部中更容易实现和使用。[逍遥子笔记:对这种方法还有一些疑问,我的理解,版本应该标识资源,也就是版本是针对URL尾部的端点,例如https://api.example.com/vi/zoos中的/zoos,按照这种思路,当一个项目包含不少类型的资源,这些资源须要要分级进行展现,此时版本号放在URL里可能遇到不少问题,例如:有个即时通讯项目IM,它包含user_info(用户信息),user_rel(用户关系),message三个资源大类,其中user_info又分为公开信息public和私有信息private两个类型,public类型包括我的简介的二维码信息self_info,private类型包括我的的应用锁信息lock, user_rel(用户关系)类型包括好友关系friends和群组groups两个类型,groups包含群成员member和群信息info两个子类型资源,messge包含新聊天消息new和历史聊天消息history,这些资源组织成树状结构后以下图所示:
假设个人URL根(在本文后面将会介绍URL的根)为:https://test.jason.com/im/*,那么上述资源对应的URL为:
(1)https://test.jason.com/im/user_info/public/self_info
该URL表示资源:用户我的简介的二维码信息;
(2)https://test.jason.com/im/user_info/private/lock
该URL表示资源:用户的应用锁信息;
(3)https://test.jason.com/im/user_rel/friends
该URL表示资源:用户的好友信息
(4)https://test.jason.com/im/user_rel/groups/member
该URL表示资源:群组的群成员
(5)https://test.jason.com/im/user_rel/groups/info
该URL表示资源:群组的信息
(6)https://test.jason.com/im/message/new
该URL表示资源:用户新聊天消息
(7)https://test.jason.com/im/message/history
该URL表示资源:用户的历史聊天消息
问题是:在这种状况下版本号应该放在URL的那个地方?
若是版本号是针对IM整个项目,例如这里的IM项目总体分为v1和v2两个大版本,此时的URL首部就能改为:https://test.jason.com/im/v1/*,https://test.jason.com/im/v2/*,实际的URL资源将变成:https://test.jason.com/im/v1/message/new。
若是版本号是针对IM项目中的某个子类,例如这里的message类型分为v1和v2两个版本,那么消息类的URL根就会变成:https://test.jason.com/im/message/v1/*,https://test.jason.com/im/message/v2/*,实际的URL资源将会变成:https://test.jason.com/im/message/v1/new。
若是版本号针对的资源类型更详细,那么版本号就会更靠后。在一些负责类型的项目中,资源的类型也会很是复杂,层级也更深,按照本文做者建议的方法就很难肯定v1的位置]
跟踪各版本/端点的API被调用的状况[逍遥子笔记:因而可知版本号对应URL末尾的“端点”]。能够经过在数据库中为每一个API增长一个计数器来实现,来一个请求就将对应的使用计数加1。统计各API的调用状况会带来不少好处,例如,优化调用频度最高的API。
为了构建第三方开发人员喜欢的API,最重要事情是肯定什么时候不建议(deprecate)用户使用旧版本API,你可使用这些不建议的(deprecated)API来告知第三方开发人员,这是在你废掉旧版本以前提醒他们的一个好途径。
通知第三方开发人员的过程能够自动化完成,例如,每调用10,000次deprecated API就给相应开发人员发一个邮件提醒。
不管你是否相信,API的根设计很是重要。当开发人员接手一个使用你的API所开发的旧项目,而且须要为它增长新特性的时候,他可能彻底不了解你的服务,或许他们知道的就是所调用的一系列URL。重要的是你API的URL根应该尽量简单,一个又长又复杂的URL看起来就吓人,它极可能就把这些第三方开发人员吓跑了。
这里有两个普通的URL根:
若是你的应用程序很庞大,或者将来它可能变得很庞大,你能够把API放在各自的子域内,这么作可让你的程序在之后的发展中更灵活、更容易扩展。[逍遥子笔记:这里做者想表达的意思好像是把URL所表示的资源进行分类、分层级,不一样的资源放在不一样的类中]
若是你的程序不会变得这么大,或者你想简化程序的使用(例如,你想经过一个框架同时提供网站和API服务),就把你的API放在URL的根域(例如:/api/)以后。
最好让你API的根也包含内容。例如,访问githubAPI的根就会获得一个端点(端点表明资源)列表。我更偏好使用根URL获取那些对“正在迷茫中的”开发人员来讲有用的信息,例如:怎么获取API的开发者文档。
注意使用HTTPS前缀,一个好的RESTfulAPI必须使用HTTPS做为前缀。[逍遥子笔记:例如:https://api.example.com/v1/zoos]
端点是URL中用于标识一个特定资源或资源集合的那部分URL片断。[逍遥子笔记:例如:https://api.example.com/v1/zoos中的/zoos]
假如你想构建用于表示多个动物园资源的API,其中,每一个动物园都包含不少动物(每一个动物只能属于一个动物园),顾员(他们能够在多个动物园工做),而且须要跟踪每一个动物的详细信息,那么这些API的端点可能以下所示:[逍遥子笔记:下列URL中红色加粗的后缀就是端点]
在介绍这些端点的做用时,你须要给出这些端点以及操做它们的HTTP动做。例以下面给出的是刚才所构建动物园API列表的功能,注意,我在每一个端点前面都加上了HTTP动做,就像HTTP请求中所使用的那样。
上述列表中,ZID表示动物园的ID,AID表示动物ID,EID表示雇员ID,ATID表示动物类型ID,在文档中给出关键词及含义是一个很是好的习惯。
在上面的例子中我已简要列出常见API的URL前缀,这种简化方式(省略URL前缀)很是有利于沟通,可是在你的API文档当中,仍是要使用所有的URL(例如:GET https://api.example.com/vi/animal_type/ATID)。[逍遥子笔记:在非正式文档中介绍一个API时能够采用端点代替完成URL这种方法:HTTP动做+端点+该端点的功能说明,端点要比完整URL短的多,这样更容易表述,也不影响理解,可是在正式文档中仍是要把URL写全]
这里须要注意数据之间关系的展现,尤为是雇员和动物园之间的多对多的关系。你能够经过增长URL的方式来表示更多的数据关系[逍遥子笔记:根据RoyThomas Fielding对资源的解释,关系也是一种资源,所以,在遇到多对多的数据关系时,能够将数据关系进行拆分,并为每一个关系都增长一个URL]。固然,这里并无一个HTTP操做能表示解除雇员,可是咱们能够经过删除指定动物园的雇员的方式来达到一样的效果。[逍遥子笔记:这是对上面“DELETE/zoos/ZID/employees/EID:解雇指定动物园的某个雇员”这个条目的解释]
当用户请求获取一组对象列表时,你就须要对结果进行过滤并返回一组严格符合用户要求的对象。有时返回结果的数量可能很是大,可是你也不能随意对此进行约束,由于这种服务端的随意约束会形成第三方开发人员的困惑。若是用户请求了一个集合,并对返回结果进行遍历,而后只要前100个对象,那么这里就须要由用户来指明这个限制量。这样用户就不会有这样的疑惑:是他们程序的bug仍是接口限制了100条?仍是网络只容许传这么大的包?[逍遥子笔记:在IM项目中有个接口让用户拉取本身的历史消息,咱们就须要在接口中增长一个参数让用户来肯定本次要拉取多少条历史消息,服务器端根据用户传入的限制量来肯定返回消息的条数,而不是由服务器来在实现时就肯定该接口一次调用只能返回几条]
尽可能减小对第三方开发人员的随意约束。[逍遥子笔记:不要在接口中添加默认的约束条件]
很是重要的一点:让第三方开发人员本身指定排序过滤器/返回结果集的约束条件。这么作的最重要缘由是:用户能用尽可能少的网络消耗尽快获取到结果;第二个重要缘由是:用户可能很懒,想让服务端帮他们作好分页和过滤;还有一个对客户端不那么重要,可是对服务端很重要的缘由:这么作会下降请求的资源负载。
过滤器一般用于过滤GET请求返回的资源集合,在GET请求中,能够经过URL传递过滤器信息。你能够放心把下面例子中列出的过滤器类型应用到本身的API中:
上述部分过滤器与前面介绍的某些URL端点功能重复,例如前面提到的URL:GET /zoo/ZID/animals在功能上就与使用过滤器的GET /animals?zoo_id=ZID重复。功能单一的端点对于第三方开发人员来讲更容易使用,尤为是他们使用你提供的请求作一些复杂的开发时,更是如此。在API文档中明确写出这些功能重复的请求方式,将会消除第三方开发人员对这些重复功能的困惑,不然他们就会怀疑这些重复功能之间是否有差别!
还有一点,当须要对数据进行过滤或者排序时,你要给第三方开发人员(Consumer)列出哪些属性能用于过滤或排序,咱们不但愿把任何数据库操做的错误返回给用户。[逍遥子笔记:不要把服务内部的错误或者问题暴露给第三方开发人员]
充分利用适当的状态码对于设计REST 风格的API来讲很是重要,由于HTTP状态码已有标准定义,而且各类网络设备都能读取并识别这些它们。例如,经过配置负载均衡器的参数来避免将请求发往出现50X错误的服务程序[逍遥子笔记:50X表示服务程序内部出错]。这里将列出一些可供你选择使用的HTTP状态码,它们能够成为你设计良好返回码的出发点:
客户端向服务器请求数据时,服务器找到这些数据并将之返回给客户端(此行为幂等);[逍遥子笔记:GET操做只能获取数据(即只读),不该该对服务器的数据进行任何形式的修改]
客户端向服务器发送数据,服务器为这些数据建立一个资源;
客户端请求服务器删除一个资源时,服务器将该资源删除;[逍遥子笔记:返回码204表示执行成功了,可是没有数据。HTTP 的RFC2616中对于204返回码的描述为:若是客户端是个代理(例如浏览器),它不该该改变“触发该请求的”页面展现,该返回值主要用于输入行为发生时,虽然新的或更新过的元数据信息被应用于当前页面,但代理(浏览器)不能改变当前的页面展现,原文为:
If the client is a user agent, itSHOULD NOT change its document view from that which caused the request to besent. This response is primarily intended to allow input for actions to takeplace without causing a change to the user agent’s active document view,although any new or updated metainformation SHOULD be applied to the documentcurrently in the user agent’s active view.]
客户端给服务器发送了一个无效的请求,服务器对此不做任何动做(此行为幂等)。
客户端请求了一个不存在的资源或资源集合,服务端对此不做任何动做(此行为幂等)。
服务器内部发生了错误,客户端没法知道请求是否被执行成功了。
1XX的返回码预留给HTTP的底层使用,在你的整个职业生涯中都不会主动发送这种返回码;
2XX的返回码表示请求按照预期执行并成功返回了信息。服务端要尽量给用户返回这种结果。
3XX的返回码表示请求重定向,大多数API都不会常用这种请求(),可是最新的超媒体API会充分使用这些功能。
4XX的返回码主要表示由客户端引发的错误,例如请求参数错误或者访问一个不存在的资源,这些必须为幂等操做,而且不能改变服务器的状态[逍遥子笔记:其实服务器的状态发生了改变就意味着操做不是幂等了]。
5XX的返回码主要表示由服务器引发的错误,一般状况下,这些错误都是开发人员([逍遥子笔记:这里应该是服务器程序的开发人员])接触不到的底层函数抛出来,而后传递给用户([逍遥子笔记:这里应该是第三方开发人员])的。用户在收到5XX的返回码时,他们不知道服务器当前的工做状态是否正常,所以,要尽可能避免这种状况发生。
当第三方开发人员在发送HTTP请求时,他们须要事先了解这些请求的返回值信息,下述列表就是一些REST风格API以及它们对应的返回值信息:[逍遥子笔记:与前面的介绍URL的功能相似,只是这里将最后面的URL描述换成返回值描述,这里依然采用三段式描述法:HTTP请求动做+端点+端点对应的返回值描述信息]
须要注意的是:当用户建立一个资源时,他们一般并不想知道新建立资源的ID(也不想知道其余属性,例如修改或建立的时间戳)[逍遥子笔记:这一点略有疑惑,这种设计思路应该也是区分场合的,若是资源很复杂,本文介绍的这种思路或许可行,若是资源本来就很简单,例如咱们之前设计的发送消息接口,就直接给用户返回所发送消息的ID]。这些新增的属性信息可经过后续请求得到,固然也能够经过初始化POST请求来返回。
多数状况下,服务器想确切知道每一个请求的发起方是谁?固然,部分API接口能放开被用匿名访问,但一般接口只能被受权的用户访问。
OAuth2.0为此提供了一个很好的实现途径。你能知道每一个请求是由哪一个客户端发起?这些请求背后分别表明了哪些用户?以及提供一种标准化的用户访问或撤销访问方式,全部这些都无需第三方用户的登陆授信。
还有OAuth1.0和xAuth也能完成相似功能。不管采用那种方法,必定要确保通用性以和良好的文档设计,在文档中对用户经常使用语言/平台的各类不一样封装库进行详细说明。[逍遥子笔记:这些服务以库的方式供客户端调用,例如SDK,所以在文档中要对各类形式的封装库进行详细说明]
我能够如实地告诉你,尽管OAuth1.0a虽然是最安全的选项,可是它很是难以部署。我遇到不少第三方开发人员抱怨他们不得不实现本身的库,由于OAuth1.0a没有他们所使用语言对应的库。我花费了大量的时间用于解决那些难以理解的“invalid signature”错误。所以,我建议你使用其余的替代方案。
目前,大多数REST风格API接口都提供JSON格式的数据,你所能想起来的Facebook、Twitter、GitHub都是如此。XML方式已经逐渐退出人们的视野(除了一些大公司内部还在使用以外),幸好SOAP方式已经消失,咱们已经看不到返回HTML格式数据的API接口了。
开发者经常使用的开发语言或框架都能轻易解析你返回的各类有效数据。若是你正在使用不一样的序列化器构建一个通用的返回对象,你可使用前面提到的任意数据格式(SOAP除外),不过在返回数据时须要注意处理请求首部的Accept字段。[逍遥子笔记:HTTP请求头部的Accept可用于指定返回数据的格式]
一些API开发人员建议针对不一样返回内容的类型,为URL(在端点以后)添加扩展字段,例如:.json,.xml,或者.html,但我不建议这么作,我建议使用HTTP请求首部的Accept字段(HTTP的RFC文档中有对Accept的解释),而且以为这才是合适的方法。
超媒体API可能表明REST风格API的将来发展,它们在思想上更符合HTTP和HTML的设计初衷。[逍遥子笔记:根据REST做者Roy Thomas Fielding的描述,REST核心是面向资源的设计,超媒体服务所提供了各类多媒体资源的访问,它在本质上符合了以资源为中心的设计]
在使用非超媒体的REST风格API时,URL端点也是客户端和服务器之间契约的一部分,这些端点必须事先告知客户端,一旦改变它们,客户端就没法按预期与服务器进行交互,这其实也是一种约束。
如今,API的用户不只仅是能发起HTTP请求的用户代理,人们更经常使用浏览器发起HTTP请求。然而,用户不会被这些预先定义的、REST风格API的URL端点所约束。是什么让用户变得如此特殊呢?如今的网页能让用户先读主题,而后点击他们感兴趣的主题所对应的连接,访问他们想访问的网站或者想阅读的内容,此时URL发生变化的时候,用户不受影响(除非用户为某个网页打了标签,在他们访问这些打了标签的网页时会自动跳转到主页,用户须要再从主页中寻找他们所感兴趣数据的新路径)。[逍遥子笔记:咱们的网站一般采用这种方式,尤为是各类门户网站,例如网易:www.163.com,新浪:www.sina.com,打开这些门户网站,咱们看不到一个URL,咱们所看到的都是一个个的主题,每一个主题对应一个超连接(URL),当这些主题对应的URL发生变化时,只须要调整主题和超连接(URL)的映射关系便可,用户实际上看不到这些URL的变化]
超媒体API概念和一个普通人的行为相似。请求API的根目录将会得到一个URL列表,这些URL列表的每一项均可能对应了一个信息集,而且它以用户可以理解的方式来描述这些信息集合。无需为每一个资源提供ID(除非特别邀请),由于一个URL就惟一标识了一个资源。
超媒体API用户在访问链接而且收集信息时,返回结果中将被放入最近更新的URL连接,所以URL无需事先与用户约定。若是URL被缓存了,后续请求又返回了404[逍遥子笔记:404表示请求了一个不存在的资源],用户只需简单地回到根目录从新寻找内容便可。
当从集合中检索一个资源列表时,要给用户返回这个资源列表的完整URL。当执行POST/PATCH/PUT请求时,可用返回码为3XX的响应来重定向到新的资源。
JSON即没法给咱们提供须要的语义来指明哪些属性是URL,也不能说明URL怎样与当前的文档关联起来;可能正如你猜想的那样,HTML能够提供这样的信息。咱们能够先经过API拿到数据,而后再进行HTML处理。想一想CSS陪伴咱们走过的这些路,咱们可能有一天会看到:在获取一样URL和内容的时候,不管经过API请求仍是网站访问,都采用同一种处理方式。[逍遥子笔记:JSON提供数据内容,并未提供数据展现功能]
老实说,若是没有彻底按照本文档的指导,你的API也不必定会太差,然而,若是你没有为API编写合适的文档,没有人会愿意用它,它将变为一个极难使用的API。
确保你的文档无需受权即可被开发人员访问。
不要使用自动文档生成器,若是用了,就必定要仔细检查和修改生成的文档,确保它们可以被用户理解和使用。
文档中所举例子中的请求和响应包体要写全,不要截断不重要的部分,而是用高亮方式展现重要的部分。
文档中要写明各URL端点的预期返回码和可能的出错信息,以及出现这些错误信息的可能缘由是什么?
若是时间充足,你还能够构建一个第三方开发人员使用的API控制台,以便他们能够当即对你的API进行验证。这个事情作起来不会像你想象的那么难,可是开发人员(包括内部开发人员和第三方开发人员)却可能由于这个功能而喜欢上使用你的API。
确保文档能被打印,例如,CSS就是一种强大的文档展现方式;在文档打印的时候必定要隐藏文档的工具栏,即使没有人把你的文档用打印机打印出来,也会有不少开发人员喜欢把它们输出为PDF以便离线查看。
原贴 : https://blog.csdn.net/houjixin/article/details/54315835