近一年半,我参与了两到三个项目的工做,这些项目涉及到大量供“外部”使用的REST API,稍后咱们会看到为何要将“外部”这个词放在引号之中。在项目工做期间,我不得不对这些API进行反复地设计,再设计和重构,这篇文章是我对Rest API最佳实践的一些我的见解,但愿读者可以从中获益。javascript
对于不少语言来讲,实现REST Service是一项极其微不足道的任务。换言之,不管你选择什么底层框架,只要辅以少许配置和代码,你能够在一小时以内就拥有一个REST Service。虽然对于缺少经验的人来讲,这确实很方便,但它也很容易让你迅速写出一个质量低下的API。所以,在你编写代码以前,先留出一分钟的时间思考一下,试着去设计你的API,花足够的时间去理解业务范畴,判断客户端须要从你的系统中获取什么。举个例子,若是你的系统是针对一群硬币收藏家所创建的数据库,此时你须要决定的是:你是否容许客户端添加新的硬币,或者仅仅容许取出原有的硬币;客户须要什么样的查询方式;若是赶上涉及大量数据检索的请求,你如何处理它?尽早地回答这些问题可以帮助你开发出更贴近用户需求的API。java
如今已经颇有多关于资源(Resource)命名和组织的讨论了,在这里我基于本身的经验再老调重弹一下,如下是三种易于遵循的规范。数据库
1. 只使用名词:举个例子,若是你想提供一项在数据库中搜索硬币的服务,要避免将端点(Endpoint)命名为/searchCoins或/findCoins或/getAllCoins 等等,一个简单的/coins就已经足够了,当客户端发送一个GET请求的时候,能够得到全部有效硬币的集合。相似的,若是你想提供一项在数据库中添加硬币的服务,要避免使用诸如/addCoin或/saveCoin或/insertCointToDatabase这样的名称,你可使用与上面相同的资源名称,要改变的仅仅是用POST请求代替GET请求。一样地,对于更新硬币,可使用PUT请求。json
2. 若是须要获取单个硬币,又应该怎么作呢?我所建议的最佳方式是在端点中加入一个参数,好比说客户端须要拿到一个ID是20的硬币,那么发送一个请求到/coins/20就足够了。咱们再来看一个更复杂的例子,若是要让客户端可以为每一个硬币添加一张图片,一个快速而丑陋的方式是/addCoinImage或/addNewImageToCoin等等,一个稍好一点的方式是/coins/addImage,可是正如我以前所说的,不该该有任何动词存在。还记得咱们以前提到的获取某种硬币的方法吗?咱们能够将其稍微加强一下,发送POST请求给/coins/20/images如何?目前看起来很不错。不过天下没有完美的事物,假设一下,若是咱们要让一些超级用户可以从系统中删除硬币,根据咱们以前的讨论,一个简单的DELETE请求发送给/coins/{id}就足够了,可是请你想一下,若是{id}仅仅是COINS表中的一个顺序编号,那会产生多大的问题?某人能够轻易地一个接一个的发送DELETE请求,最后系统中全部的数据全没了。我想说的重点是,使用标识符做为请求参数是不错,可是前提是这些标识符必须很难猜想或根本没法猜想。因此,若是你想要用一串序号去肯定一个实体,那就忘了这种实现吧。个人建议是,不要使用资源参数,直接发送一个DELETE请求给/coins,结合一个request body(好比json),其中含有足够的参数可以定位所要被删除的实体便可。安全
3. 尽量使用特定领域的名称。若是你的业务域中有一群硬币收藏家(Coin Collectors),那么当你设计API的时候,应当使用collectors这个词,而不是users或accounts。要避免使用一些意义过于宽泛的名称,这些名称不能表示什么,到了客户端又容易产生误解。对于请求参数的命名,道理也是同样的。另外,强烈建议给请求参数取一个尽量短,同时又有意义的名称,举个例子,若是你想要查找在某一指定年份发行的硬币,一个很赞的参数名称是issueYear,比较典型的反例是:year(意义不明确),yearOfFirstIssue(包含无用信息)。服务器
对于这个话题,个人经验是让客户端在每次发送请求后,不管结果是成功仍是失败,都能得到相同格式的json响应,这将会给客户端处理带来极大的帮助。举个例子,你想要添加一个新的硬币,向/coins发送POST请求,一个成功的响应包含如下json文档:框架
1
2
3
4
5
6
7
8
|
{
"meta"
:{
"code"
:200
},
"data"
:{
"coinId"
:
"a7sad-123kk-223"
}
}
|
一个错误的响应多是这样的:工具
1
2
3
4
5
6
7
8
9
|
{
"meta"
:{
"code"
:60001,
"error"
:
"Can not add coin"
,
"info"
:
"Missing one ore more required fields"
},
"data"
:{
}
}
|
请注意,对全部可能的结果(成功或失败),json响应的文档都具有相同的结构,其中有两种基本元素:meta和data,meta包含结果信息,在出错的状况下,其中还会包含一个特殊的错误码(error code),在错误码以后,”error”表示出错的内容,”info”表示出错的具体描述;data是可选的,包含从服务器返回的全部数据,就拿上面的例子来讲,当添加硬币成功后,服务器会返回一个惟一的自动生成的标识符,若是有错误,这项就为空。这种作法的优点是,对于同一个API的各类服务类型和结果,客户端均可以采用相同的方式进行处理。此外,当有意外状况发生时,咱们也能够传递一些额外的信息,正如上面例子中所展现的,”error”传达信息,”info”记录日志。咱们还有一种选择,能够基于错误码去处理响应,只要明确每一个数字的含义便可,请注意这些数字并不是http状态码,你依然要为每一个请求返回正确的http状态码(如400、401等)。ui
在咱们讨论下一节以前,我想强调另外一件值得重视的事,假设咱们不容许删除硬币,可是客户端尝试向/coins/{id}发送一个DELETE请求,一般状况下Web容器会返回一个405的状态码,但我发现,若是咱们对这些响应进行过滤并返回相同的json文档,会颇有帮助。好比咱们能够返回:spa
1
2
3
4
5
6
7
8
9
|
{
"meta"
:{
"code"
:405,
"error"
:
"Method not allowed for the /coins/{id} resource"
,
"info"
:
"Method DELETE is not allowed for that resource. Available methods : GET, POST, OPTIONS"
},
"data"
:{
}
}
|
这比原来好多了,不是吗?如今,响应内容不但包含原有的信息(405状态码),还通知客户端该资源可用的方法。
最后但也是最重要的一点,花一点时间,提供一份专业的、对开发人员友好的文档,并保证及时更新,一份过时文档的危害性比没有文档更甚。你可使用一些开源免费的工具对你的API进行文档化。再好一点的作法是,对每一项资源的使用方式都能提供范例,对成功或错误的响应都能提供预期结果。不要忘了,在最后要记录下每个错误码并提供完整的信息,这样客户端才能在错误发生时作出反应,有一些客户端不会理会你的响应内容,它们会根据你的错误码自行提供信息。
我还有若干个更为实用的建议待写,特别是关于API的版本控制和安全性方面的建议,但我想它们更适合在另外一篇博文中进行探讨。
http://blog.jobbole.com/70511/