转:RESTful API 设计最佳实践

目前互联网上充斥着大量的关于RESTful API(为了方便,后面API和RESTful API 一个意思)如何设计的文章,然而却没有一个“万能”的设计标准:如何鉴权?API格式如何?你的API是否应该加入版本信息?当你开始写一个app的时候,特别是后端模型部分已经写完的时候,你不得不殚精竭虑的设计和实现本身app的public API部分。由于一旦发布,对外发布的API将会很难改变。 

在给SupportedFu设计API的时候,我试图以实用的角度来解决上面提到的问题。我但愿能够设计出容易使用,容易部署,而且足够灵活的API,本文所以而生。 

API设计的基本要求 

网上的不少关于API设计的观点都十分”学院派“,它们也许更有理论基础,可是有时却和现实世界脱轨(所以我是自由派)。因此我这篇文章的目标是从实践的角度出发,给出当前网络应用的API设计最佳实践(固然,是我认为的最佳了~),若是以为不合适,我不会听从标准。固然做为设计的基础,几个必须的原则仍是要遵照的: 

html

  1. 当标准合理的时候遵照标准。
  2. API应该对程序员友好,而且在浏览器地址栏容易输入。
  3. API应该简单,直观,容易使用的同时优雅。
  4. API应该具备足够的灵活性来支持上层ui。
  5. API设计权衡上述几个原则。

须要强调的是:API的就是程序员的UI,和其余UI同样,你必须仔细考虑它的用户体验! 

使用RESTful URLs 和action 

虽然前面我说没有一个万能的API设计标准。但确实有一个被广泛认可和遵照:RESTfu设计原则。它被Roy Felding提出(在他的”基于网络的软件架构“论文中第五章)。而REST的核心原则是将你的API拆分为逻辑上的资源。这些资源经过http被操做(GET ,POST,PUT,DELETE)。 

那么我应该如何拆分出这些资源呢? 

显然从API用户的角度来看,”资源“应该是个名词。即便你的内部数据模型和资源已经有了很好的对应,API设计的时候你仍然不须要把它们一对一的都暴露出来。这里的关键是隐藏内部资源,暴露必需的外部资源。 

在SupportFu里,资源是 ticket、user、group。 

一旦定义好了要暴露的资源,你能够定义资源上容许的操做,以及这些操做和你的API的对应关系: 

java

  • GET /tickets # 获取ticket列表
  • GET /tickets/12 # 查看某个具体的ticket
  • POST /tickets # 新建一个ticket
  • PUT /tickets/12 # 更新ticket 12.
  • DELETE /tickets/12 #删除ticekt 12

能够看出使用REST的好处在于能够充分利用http的强大实现对资源的CURD功能。而这里你只须要一个endpoint:/tickets,再没有其余什么命名规则和url规则了,cool! 

这个endpoint的单数复数 

一个能够听从的规则是:虽然看起来使用复数来描述某一个资源实例看起来别扭,可是统一全部的endpoint,使用复数使得你的URL更加规整。这让API使用者更加容易理解,对开发者来讲也更容易实现。 

如何处理关联?关于如何处理资源之间的管理REST原则也有相关的描述: 

python

  • GET /tickets/12/messages- Retrieves list of messages for ticket #12
  • GET /tickets/12/messages/5- Retrieves message #5 for ticket #12
  • POST /tickets/12/messages- Creates a new message in ticket #12
  • PUT /tickets/12/messages/5- Updates message #5 for ticket #12
  • PATCH /tickets/12/messages/5- Partially updates message #5 for ticket #12
  • DELETE /tickets/12/messages/5- Deletes message #5 for ticket #12


其中,若是这种关联和资源独立,那么咱们能够在资源的输出表示中保存相应资源的endpoint。而后API的使用者就能够经过点击连接找到相关的资源。若是关联和资源联系紧密。资源的输出表示就应该直接保存相应资源信息。(例如这里若是message资源是独立存在的,那么上面 GET /tickets/12/messages就会返回相应message的连接;相反的若是message不独立存在,他和ticket依附存在,则上面的API调用返回直接返回message信息)

不符合CURD的操做 

对这个使人困惑的问题,下面是一些解决方法: 

git

  • 重构你的行为action。当你的行为不须要参数的时候,你能够把active对应到activated这个资源,(更新使用patch).
  • 以子资源对待。例如:GitHub上,对一个gists加星操做:PUT /gists/:id/star 而且取消星操做:DELETE /gists/:id/star.
  • 有时候action实在没有难以和某个资源对应上例如search。那就这么办吧。我认为API的使用者对于/search这种url也不会有太大意见的(毕竟他很容易理解)。只要注意在文档中写清楚就能够了。
  • 永远使用SSL

毫无例外,永远都要使用SSL。你的应用不知道要被谁,以及什么状况访问。有些是安全的,有些不是。使用SSL能够减小鉴权的成本:你只须要一个简单的令牌(token)就能够鉴权了,而不是每次让用户对每次请求签名。 

值得注意的是:不要让非SSL的url访问重定向到SSL的url。 

文档 

文档和API自己同样重要。文档应该容易找到,而且公开(把它们藏到pdf里面或者存到须要登陆的地方都不太好)。文档应该有展现请求和输出的例子:或者以点击连接的方式或者经过curl的方式(请见openstack的文档)。若是有更新(特别是公开的API),应该及时更新文档。文档中应该有关于什么时候弃用某个API的时间表以及详情。使用邮件列表或者博客记录是好方法。 

版本化 

在API上加入版本信息能够有效的防止用户访问已经更新了的API,同时也能让不一样主要版本之间平稳过渡。关因而否将版本信息放入url仍是放入请求头有过争论:API version should be included in the URL or in a header. 学术界说它应该放到header里面去,可是若是放到url里面咱们就能够跨版本的访问资源了。。(参考openstack)。 

strip使用的方法就很好:它的url里面有主版本信息,同时请求头俩面有子版本信息。这样在子版本变化过程当中url的稳定的。变化有时是不可避免的,关键是如何管理变化。完整的文档和合理的时间表都会使得API使用者使用的更加轻松。 

结果过滤,排序,搜索: 

url最好越简短越好,和结果过滤,排序,搜索相关的功能都应该经过参数实现(而且也很容易实现)。 

过滤:为全部提供过滤功能的接口提供统一的参数。例如:你想限制get /tickets 的返回结果:只返回那些open状态的ticket–get /tickektsstate=open这里的state就是过滤参数。 

排序:和过滤同样,一个好的排序参数应该可以描述排序规则,而不业务相关。复杂的排序规则应该经过组合实现: 

程序员

  • GET /ticketssort=-priority- Retrieves a list of tickets in descending order of priority
  • GET /ticketssort=-priority,created_at- Retrieves a list of tickets in descending order of priority. Within a specific priority, older tickets are ordered first

这里第二条查询中,排序规则有多个rule以逗号间隔组合而成。 

搜索:有些时候简单的排序是不够的。咱们可使用搜索技术(ElasticSearch和Lucene)来实现(依旧能够做为url的参数)。 

github

  • GET /ticketsq=return&state=open&sort=-priority,created_at- Retrieve the highest priority open tickets mentioning the word ‘return’

对于常用的搜索查询,咱们能够为他们设立别名,这样会让API更加优雅。例如: 

编程

代码 
  1. get /ticketsq=recently_closed -> get /tickets/recently_closed.  



限制API返回值的域 

有时候API使用者不须要全部的结果,在进行横向限制的时候(例如值返回API结果的前十项)还应该能够进行纵向限制。而且这个功能能有效的提升网络带宽使用率和速度。可使用fields查询参数来限制返回的域例如: 

json

代码 
  1. GET /ticketsfields=id,subject,customer_name,updated_at&state=open&sort=-updated_at  



更新和建立操做应该返回资源 

PUT、POST、PATCH 操做在对资源进行操做的时候经常有一些反作用:例如created_at,updated_at 时间戳。为了防止用户屡次的API调用(为了进行这次的更新操做),咱们应该会返回更新的资源(updated representation.)例如:在POST操做之后,返回201 created 状态码,而且包含一个指向新资源的url做为返回头 

是否须要 “HATEOAS“ 

网上关因而否容许用户建立新的url有很大的异议(注意不是建立资源产生的url)。为此REST制定了HATEOAS来描述了和endpoint进行交互的时候,行为应该在资源的metadata返回值里面进行定义。(译注:做者这里认为HATEOAS还不算成熟,读者感兴趣能够本身去原文查看) 

只提供json做为返回格式 

如今开始比较一下XML和json了。XML即冗长,难以阅读,又不适合各类编程语言解析。固然XML有扩展性的优点,可是若是你只是将它来对内部资源串行化,那么他的扩展优点也发挥不出来。不少应用(youtube,twitter,box)都已经开始抛弃XML了,我也不想多费口舌。给了google上的趋势图吧: 

固然若是的你使用用户里面企业用户居多,那么可能须要支持XML。若是是这样的话你还有另一个问题:你的http请求中的media类型是应该和accept 头同步仍是和url?为了方便(browser explorability),应该是在url中(用户只要本身拼url就行了)。若是这样的话最好的方法是使用.xml或者.json的后缀。 

命名方式? 

是蛇形命令(下划线和小写)仍是驼峰命名?若是使用json那么最好的应该是遵照JAVASCRIPT的命名方法-也就是说骆驼命名法。若是你正在使用多种语言写一个库,那么最好按照那些语言所推荐的,java,c#使用骆驼,python,ruby使用snake。 

我的意见:我总以为蛇形命令更好使一些,固然这没有什么理论的依据。有人说蛇形命名读起来更快,能达到20%,也不知道真假http://ieeexplore.ieee.org/xpl/articleDetails.jsptp=&arnumber=5521745 

默认使用pretty print格式,使用gzip 

只是使用空格的返回结果从浏览器上看老是以为很恶心(一大坨有没有?~)。固然你能够提供url上的参数来控制使用“pretty print”,可是默认开启这个选项仍是更加友好。格外的传输上的损失不会太大。相反你若是忘了使用gzip那么传输效率将会大大减小,损失大大增长。想象一个用户正在debug那么默认的输出就是可读的-而不用将结果拷贝到其余什么软件中在格式化-是想起来就很爽的事,不是么? 

下面是一个例子: 

c#

代码 
  1. $ curl https://API.github.com/users/veesahni > with-whitespace.txt  
  2. $ ruby -r json -e 'puts JSON JSON.parse(STDIN.read)' < with-whitespace.txt > without-whitespace.txt  
  3. $ gzip -c with-whitespace.txt > with-whitespace.txt.gz  
  4. $ gzip -c without-whitespace.txt > without-whitespace.txt.gz  



输出以下: 

后端

代码 
  1. without-whitespace.txt- 1252 bytes  
  2. with-whitespace.txt- 1369 bytes  
  3. without-whitespace.txt.gz- 496 bytes  
  4. with-whitespace.txt.gz- 509 bytes  



在上面的例子中,多余的空格使得结果大小多出了8.5%(没有使用gzip),相反只多出了2.6%。听说:twitter使用gzip以后它的streaming API传输减小了80%(link:https://dev.twitter.com/blog/announcing-gzip-compression-streaming-APIs). 

只在须要的时候使用“envelope” 

不少API象下面这样返回结果: 

代码 
  1. {  
  2.   "data" : {  
  3.     "id" : 123,  
  4.     "name" : "John"  
  5.   }  
  6. }  



理由很简单:这样作能够很容易扩展返回结果,你能够加入一些分页信息,一些数据的元信息等-这对于那些不容易访问到返回头的API使用者来讲确实有用,可是随着“标准”的发展(cors和http://tools.ietf.org/html/rfc5988#page-6都开始被加入到标准中了),我我的推荐不要那么作。 

什么时候使用envelope? 

有两种状况是应该使用envelope的。若是API使用者确实没法访问返回头,或者API须要支持交叉域请求(经过jsonp)。 
jsonp请求在请求的url中包含了一个callback函数参数。若是给出了这个参数,那么API应该返回200,而且把真正的状态码放到返回值里面(包装在信封里),例如: 

代码 
  1. callback_function({  
  2.   status_code: 200,  
  3.   next_page: "https://..",  
  4.   response: {  
  5.     ... actual JSON response body ...   
  6.   }  
  7. })  



一样为了支持没法方法返回头的API使用者,能够容许envelope=true这样的参数。 

在post,put,patch上使用json做为输入 

若是你认同我上面说的,那么你应该决定使用json做为全部的API输出格式,那么咱们接下来考虑考虑API的输入数据格式。 
不少的API使用url编码格式:就像是url查询参数的格式同样:单纯的键值对。这种方法简单有效,可是也有本身的问题:它没有数据类型的概念。这使得程序不得不根据字符串解析出布尔和整数,并且尚未层次结构–虽然有一些关于层次结构信息的约定存在但是和自己就支持层次结构的json比较一下仍是不很好用。 

固然若是API自己就很简单,那么使用url格式的输入没什么问题。但对于复杂的API你应该使用json。或者干脆统一使用json。 
注意使用json传输的时候,要求请求头里面加入:Content-Type:applicatin/json.不然抛出415异常(unsupported media type)。 

分页 

分页数据能够放到“信封”里面,但随着标准的改进,如今我推荐将分页信息放到link header里面:http://tools.ietf.org/html/rfc5988#page-6。 

使用link header的API应该返回一系列组合好了的url而不是让用户本身再去拼。这点在基于游标的分页中尤其重要。例以下面,来自github的文档 

代码 
  1. Link: <https://api.github.com/user/repos?page=3&per_page=100>; rel="next",   
  2. <https://api.github.com/user/repos?page=50&per_page=100>; rel="last"  



自动加载相关的资源 

不少时候,自动加载相关资源很是有用,能够很大的提升效率。可是这却和RESTful的原则相背。为了如此,咱们能够在url中添加参数:embed(或者expend)。embed能够是一个逗号分隔的串,例如: 

代码 
  1. GET /ticket/12embed=customer.name,assigned_user  


对应的API返回值以下: 

代码 
  1. {  
  2.   "id" : 12,  
  3.   "subject" : "I have a question!",  
  4.   "summary" : "Hi, ....",  
  5.   "customer" : {  
  6.     "name" : "Bob"  
  7.   },  
  8.   assigned_user: {  
  9.    "id" : 42,  
  10.    "name" : "Jim",  
  11.   }  
  12. }  



值得提醒的是,这个功能有时候会很复杂,而且可能致使N+1 SELECT 问题。 

重写HTTP方法 

有的客户端只能发出简单的GET 和POST请求。为了照顾他们,咱们能够重写HTTP请求。这里没有什么标准,可是一个广泛的方式是接受X-HTTP-Method-Override请求头。 

速度限制 

为了不请求泛滥,给API设置速度限制很重要。为此 RFC 6585 引入了HTTP状态码429(too many requests)。加入速度设置以后,应该提示用户,至于如何提示标准上没有说明,不过流行的方法是使用HTTP的返回头。 

下面是几个必须的返回头(依照twitter的命名规则): 

  • X-Rate-Limit-Limit :当前时间段容许的并发请求数
  • X-Rate-Limit-Remaining:当前时间段保留的请求数。
  • X-Rate-Limit-Reset:当前时间段剩余秒数

为何使用当前时间段剩余秒数而不是时间戳? 

时间戳保存的信息不少,可是也包含了不少没必要要的信息,用户只须要知道还剩几秒就能够再发请求了这样也避免了clock skew问题。 

有些API使用UNIX格式时间戳,我建议不要那么干。为何?HTTP 已经规定了使用 RFC 1123 时间格式 

鉴权 Authentication 

restful API是无状态的也就是说用户请求的鉴权和cookie以及session无关,每一次请求都应该包含鉴权证实。 

经过使用ssl咱们能够不用每次都提供用户名和密码:咱们能够给用户返回一个随机产生的token。这样能够极大的方便使用浏览器访问API的用户。这种方法适用于用户能够首先经过一次用户名-密码的验证并获得token,而且能够拷贝返回的token到之后的请求中。若是不方便,可使用OAuth 2来进行token的安全传输。 

支持jsonp的API须要额外的鉴权方法,由于jsonp请求没法发送普通的credential。这种状况下能够在查询url中添加参数:access_token。注意使用url参数的问题是:目前大部分的网络服务器都会讲query参数保存到服务器日志中,这可能会成为大的安全风险。 

注意上面说到的只是三种传输token的方法,实际传输的token多是同样的。 

缓存 

HTTP提供了自带的缓存框架。你须要作的是在返回的时候加入一些返回头信息,在接受输入的时候加入输入验证。基本两种方法: 

  • ETag:当生成请求的时候,在HTTP头里面加入ETag,其中包含请求的校验和和哈希值,这个值和在输入变化的时候也应该变化。若是输入的HTTP请求包含IF-NONE-MATCH头以及一个ETag值,那么API应该返回304 not modified状态码,而不是常规的输出结果。
  • Last-Modified:和etag同样,只是多了一个时间戳。返回头里的Last-Modified:包含了 RFC 1123 时间戳,它和IF-MODIFIED-SINCE一致。HTTP规范里面有三种date格式,服务器应该都能处理。

出错处理 

就像html错误页面可以显示错误信息同样,API 也应该能返回可读的错误信息–它应该和通常的资源格式一致。API应该始终返回相应的状态码,以反映服务器或者请求的状态。API的错误码能够分为两部分,400系列和500系列,400系列代表客户端错误:如错误的请求格式等。500系列表示服务器错误。API应该至少将全部的400系列的错误以json形式返回。若是可能500系列的错误也应该如此。json格式的错误应该包含如下信息:一个有用的错误信息,一个惟一的错误码,以及任何可能的详细错误描述。以下: 

代码 
  1. {  
  2.   "code" : 1234,  
  3.   "message" : "Something bad happened <img src="http://blog.jobbole.com/wp-includes/images/smilies/icon_sad.gif" alt=":-(" class="wp-smiley"> ",  
  4.   "description" : "More details about the error here"  
  5. }  



对PUT,POST,PATCH的输入的校验也应该返回相应的错误信息,例如: 

代码 
  1. {  
  2.   "code" : 1024,  
  3.   "message" : "Validation Failed",  
  4.   "errors" : [  
  5.     {  
  6.       "code" : 5432,  
  7.       "field" : "first_name",  
  8.       "message" : "First name cannot have fancy characters"  
  9.     },  
  10.     {  
  11.        "code" : 5622,  
  12.        "field" : "password",  
  13.        "message" : "Password cannot be blank"  
  14.     }  
  15.   ]  
  16. }  



HTTP 状态码 

代码 
    1. 200 ok  - 成功返回状态,对应,GET,PUT,PATCH,DELETE.  
    2. 201 created  - 成功建立。  
    3. 304 not modified   - HTTP缓存有效。  
    4. 400 bad request   - 请求格式错误。  
    5. 401 unauthorized   - 未受权。  
    6. 403 forbidden   - 鉴权成功,可是该用户没有权限。  
    7. 404 not found - 请求的资源不存在  
    8. 405 method not allowed - 该http方法不被容许。  
    9. 410 gone - 这个url对应的资源如今不可用。  
    10. 415 unsupported media type - 请求类型错误。  
    11. 422 unprocessable entity - 校验错误时用。  
    12. 429 too many request - 请求过多。