RESTful API 设计最佳实践(7)
本篇博客将侧重介绍在RESTful API设计中,消息头HEADER和消息体body相关的东西。URL只是RESTful API设计的主要一部分,要实现REST的统一接口,HTTP协议中的其他部分也不可或缺。关于统一接口,可查看我之前的博客。
一、返回新建资源的URL
使用POST新建的资源时,如果创建成功,则返回201状态码,,应该在返回的消息头HEADER的 location 字段中,加上指向新建资源的URL,这是HTTP规范只一(参见rfc2616)。根据实际需要,可以在body中带上一些必要的信息。
也就是说,在RESTful API中,很多情况下,跟状态码+一些标准的header字段信息,就能够很快获知请求处理结果,而不一定非要从body中费力解析出来。但是往往我们会忽略掉这一点,而将所有处理结果信息全部塞到body中,如:
:body {:result :success :id "新资源ID"}
这种方式其实还是RPC设计思维,并且单纯返回一个新资源的ID号也是不正确的(但估计绝大多数人的实现习惯中,都是这样做的)。因为在REST中,资源是用URL定义的,一个资源ID并不能标识资源在服务其中所处的位置。客户端通过post请求新建资源时,它并不知道服务器具体把它新建到哪里了,所以服务器应当返回一个具体的URL给客户端。
二、XML vs JSON vs EDN
1. 格式的对比和选择
下图为谷歌探索中查到的XMLAPI 、JSON API以及EDN使用量的数据(搜索不到EDN数据,因此用clojure代替——用clojure开发的,大多都用edn格式吧,不是很精确)
XML API使用量越来越小,JSON API程上升趋势,clojure的使用量也在增加。如果你的服务没有被XML捆绑,那么可以完全放弃提供XML API了。如果你是clojure和clojurescript的使用者,使用edn格式是最好的,但是目前JSON格式还是更加流行,对JSON的支持库也特别丰富。如果你的服务确定只是针对公司内部,那么可以选择一个公司内部使用的格式;如果你的服务要对外提供服务,或是内外部都提供,那么你至少要支持JSON格式的消息,或者JSON+公司内部使用的消息格式。
在HTTP请求的头部,可以通过Accept
字段来告诉服务器,客户端要求返回的内容格式。如Accept: application/json;
如果服务器端正常处理了该请求,其返回消息的body中,内容应该是json格式的;如果服务器不支持json格式,则应该返回415状态码。注意:Accept是可自定义的,你可以自定义一个消息格式,只要客户端和服务器端都理解就好。
2. 几个实际问题
问题1:指定接收的媒体类型是放在请求报文头部Accept中还是直接放在URL中?
网上有资料建议放在URL中,比如在URL的后面加一个”.json”的字符串表明想要的回应消息格式,这样在浏览器中就能够很方便地调用了。这个理由是非常糟糕的。
我的建议是:在REST服务开发过程中,浏览器固然是一个非常简单、易用的调试工具,但它仅限于对GET请求的调试。但对于开发者而言,curl等命令行工具则更加强大,使用也非常简单。API的设计被浏览器这样一个调试工具所束缚,是很不合理的。除非是为了支持哪些不支持http消息头的API使用者(那些非常陈旧的HTTP实现库),可以考虑单独开放一个API,将”.json”放在URL中,但也仅限于兼容。你还是得有一个相同功能的API,将格式要求放在HEADER中。
问题2:资源及其属性命名风格的问题
建议:不同的语言使用其惯用的命名风格,你可以写一个公共的库进行格式转化。比如js/java/C#等语言使用驼峰命名法,cojure/clojurescript则使用:key以及user-id形式,python/ruby则用下划线的形式命名。
三、分页
通常,我们可能会很习惯地由客户端记录当前所处页,再组装上一页/下一页链接来进行翻页,但在REST服务中,更好的方式是由服务端组装好上一页/下一页的分页链接,随着本页信息一起回送给客户端。因为在REST服务中,是以超媒体为驱动的,因而在返回的资源信息中带有关联资源的url(超链接)是很自然的事情。如:github的处理方式如下:
Link: https://api.github.com/user/repos?page=3&per_page=100; rel=”next”,https://api.github.com/user/repos?page=50&per_page=100; rel=”last”
具体可参考github和facebook分页的API。
四、API调用频率限制
为了避免API被频繁调用(如恶意刷),通常会对每个客户端对同一个API的访问频率进行限制,在HTTP规范中,用状态码429表示请求过多。Twitter对此的处理方式是在响应头部写入限制信息,如:
*X-Rate-Limit-Limit : 当前时间段内允许访问的次数。
*X-Rate-Limit-Remaining : 当前时间段内剩余访问次数。
*X-Rate-Limit-Reset : 当前时间段剩余时间
对于超过限制的访问,将返回429状态码,并在body中给出相应提示:
{“errors” : [{“code”: 88,“message”: “Rate limit exceeded”}]}
问题3:使用timestamp时间还是使用还剩多少秒(或毫秒)的形式
建议:在http规范中规定了三种时间格式:
Sun,06 Nov 1994 08:49:37 GMT ; RFC 822,updated by RFC 1123
Sunday,06-Nov-94 08:49:37 GMT ; RFC 850,obsoleted by RFC 1036
Sun Nov 6 08:49:37 1994 ; ANSI C’s asctime() format
所以在header中表达时间时,理论上应该遵循这几种格式。
个人觉得还是看场景需要,timestamp包含的信息更多,比如有时区等信息。然而在上面要表达还剩多少秒或毫秒的场景中,直接用还剩多少秒或毫秒的形式,可能会更加直观、实用,因为API使用者只关心还要过多久才能调用。
五、失败/异常消息返回
失败/异常消息应该是可用、格式统一且易于被使用的,它也应该和系统其它资源一样,被当做一种资源返回,有它自己的属性,所以对于异常/失败这种资源,也需好好定义。
API调用成功或失败,都应该返回具有意义的状态码,而非全部返回200,然后在body中标记是否成功。错误码通常分4XX和5XX两类,下层分很多小类,我们必须仔细甄别当前异常属于哪一类。错误详细信息则可以在body中定义。
{:code 1123
:msg “错误描述信息”
:desc “更多细节信息”}
复杂的异常信息定义可能包含更多的属性描述,比如校验客户端来的数据是否合法:
{:code 1024
:msg “数据输入错误”
:errors [{:code 5432
:field :first-name
:msg “first name wrong!”}
{:code 5431
:field :password
:msg “password wrong!”}]}
这样不仅保持了整个REST服务基于资源进行抽象的一致性,也方便开发者调查异常原因,也便于灵活向用户展现错误信息。
而在我们的实际开发过程中,经常会在异常(提示)消息这些方面处理的比较随便,比如:
- 异常消息没有详细统一规划,部分异常(提示)消息在客户端定义 ,一些则由服务器发过来。
- 异常消息格式比较简单,通常就是一个message字符串,导致客户端不能灵活显示,显示的比较丑。
- 在项目开发过程中,往往忽视对异常(提示)消息进行设计,导致后来想加却难以以一种规范统一的方式加入,最后屈服于已有实现,零零碎碎四处补充一些异常信息。
六、其他
- 安全性?总是用ssl?
- 认证?
- 缓存?