(转)RESTful API 设计最佳实践

原文:http://www.oschina.net/translate/best-practices-for-a-pragmatic-restful-apihtml

数据模型已经稳定,接下来你可能须要为web(网站)应用建立一个公开的API(应用程序编程接口)。须要认识到这样一个问题:一旦API发布后,就很难 对它作很大的改动而且保持像先前同样的正确性。如今,网络上有不少关于API设计的思路。可是在所有案例中没有一种被普遍采纳的标准,有不少的选择:你接 受什么样的格式?如何认证?API应该被版本化吗?python

在为SupportFu(一个轻量级的Zendesk替换实现)设计API时,对于这些问题我尽可能得出一些务实的答案。个人目标是设计这样一个API,它容易使用和采纳,足够灵活去为咱们用户接口去埋单。git

API的关键要求

许多网上能找到的API设计观点都是些学术讨论,这些讨论是关于模糊标准的主观解释,而不是关于在现实世界中具备意义的事。本文中个人目标是,描述一下为当今的web应用而设计的实用的API的最佳实践。若是感受不对,我不会去尝试知足某个标准。为了帮助进行决策,我已经写下了API必须力争知足的一些要求:github

  • 它应当在须要的地方使用 web 标准
  • 它应当对开发者友好而且便于在浏览器地址栏中浏览和探索
  • 它应当是简单、直观和一致的,使它用起来方便和温馨
  • 它应当提供足够的灵活性来加强大多数的 SupportFu 用户界面
  • 它应当是高效的,同时要维持和其余需求之间的平衡

一个 API 是一个开发者的 UI - 就像其余任何 UI 同样, 确保用户体验被认真的考虑过是很重要的!web

使用 RESTful URLs and actions

若是有同样东西得到普遍承认的话,那就是 RESTful 原则。Roy Felding 在他论文 network based software architectures 的 第五章 中首次介绍了这些原则。shell

这些REST的关键原则与将你的 API 分割成逻辑资源紧密相关。使用HTTP请求控制这些资源,其中,这些方法(GET, POST, PUT, PATCH, DELETE)具备特殊含义。apache

但是我该整出什么样的资源呢?好吧,它们应该是有意义于 API 使用者的名词(不是动词)。虽然内部Model能够简单地映射到资源上,但那不必定是个一对一的映射。这里的关键是不要泄漏与API不相关的实现细节。一些相关的名词能够是 票,用户和小组编程

一旦定义好了资源, 须要肯定什么样的 actions 应用它们,这些 actions 怎么映射到你的 API 上。RESTful 原则提供了 HTTP methods 映射做为策略来处理 CRUD actions,以下: json

  • GET /tickets - 获取 tickets 列表
  • GET /tickets/12 - 获取一个单独的 ticket
  • POST /tickets - 建立一个新的 ticket
  • PUT /tickets/12 - 更新 ticket #12
  • PATCH /tickets/12 - 部分更新 ticket #12
  • DELETE /tickets/12 - 删除 ticket #12

REST 很是棒的是,利用现有的 HTTP 方法在单个的 /tickets 接入点上实现了显著的功能。没有什么方法命名约定须要去遵循,URL 结构是整洁干净的。 REST 太棒了! api

接入点的名称应该选择单数仍是复数呢?keep-it- simple原则能够在此应用。虽然你内在的语法知识会告诉你用复数形式描述单一资源实例是错误的,但实用主义的答案是保持URL格式一致而且始终使用复 数形式。不用处理各类奇形怪状的复数形式(好比person/people,goose/geese)可让API消费者的生活更加美好,也让API提供 者更容易实现API(由于大多数现代框架自然地将/tickets和/tickets/12放在同一个控制器下处理)。

可是你该如何处理(资源的)关系呢?若是关系依托于另一个资源,Restful原则提供了很好的指导原则。让咱们来看一个例子。SupportFu的一个ticket包含许多消息(message)。这些消息逻辑上与/tickets接入点的映射关系以下:

  • GET /tickets/12/messages - 获取ticket #12下的消息列表
  • GET /tickets/12/messages/5 - 获取ticket #12下的编号为5的消息
  • POST /tickets/12/messages - 为ticket #12建立一个新消息
  • PUT /tickets/12/messages/5 - 更新ticket #12下的编号为5的消息
  • PATCH /tickets/12/messages/5 - 部分更新ticket #12下的编号为5的消息
  • DELETE /tickets/12/messages/5 - 删除ticket #12下的编号为5的消息

或者若是某种关系不依赖于资源,那么在资源的输出表示中只包含一个标识符是有意义的。API消费者而后除了请求资源所在的接入点外,还得再请求一次关系所 在的接入点。可是若是通常状况关系和资源一块儿被请求,API能够提供自动嵌套关系表示到资源表示中,这样能够防止两次请求API。

若是Action不符合CRUD操做那该怎么办?

这是一个可能让人感到模糊不解的地方。有几种处理方法:

  1. 从新构造这个Action,使得它像一个资源的field(我理解为部分域或者部分字段)。这种方法在Action不包含参数的状况下能够奏效。例如一个有效的action能够映射成布尔类型field,而且能够经过PATCH更新资源。
  2. 利用RESTful原则像处理子资源同样处理它。例如,Github的API让你经过PUT /gists/:id/star 来 star a gist ,而经过DELETE /gists/:id/star来进行 unstar 。
  3. 有时候你实在是没有办法将Action映射到任何有意义的 RESTful结构。例如,多资源搜索没办法真正地映射到任何一个资源接入点。这种状况,/search 将很是有意义,虽然它不是一个名词。这样作没有问题 - 你只须要从API消费者的角度作正确的事,并确保所作的一切都用文档清晰记录下来了以免(API消费者的)困惑。

老是使用 SSH

老是使用SSL,没有例外。今天,您的web api能够从任何地方访问互联网(如图书馆、咖啡店、机场等)。不是全部这些都是安全的,许多不加密通讯,便于窃听或伪造,若是身份验证凭证被劫持。

另外一个优势是,保证老是使用SSL加密通讯简化了认证效果——你能够摆脱简单的访问令牌,而不是让每一个API请求签署。

要注意的一点是非SSL访问API URLs。不要重定向这些到对应的SSL。相反,抛出一个系统错误!最后一件你想要的是配置不佳的客户发送请求到一个未加密的端点,只是默默地重定向到实际加密的端点。

文档

API的好坏关键看其文档的好坏. 好的API的说明文档应该很容易就被找到,并能公开访问。在尝试任何整合工做前大部分开发者会先查看其文档。当文档被藏于一个PDF之中或要求必须登记信息时,将很难被找到也很难搜索到。

好的文档须提供从请求到响应整个循环的示例。最好的是,请求应该是可粘贴的例子,要么是能够贴到浏览器的连接,要么是能够贴到终端里的curl示例 。 GitHub 和 Stripe 在这方面作的很是出色。

一旦你发布一个公开的API,你必须承诺"在没有通告的前提下,不会更改APIDe功能" .对于外部可见API的更新,文档必须包含任何将废弃的API的时间表和详情。应该经过博客(更新日志)或者邮件列表送达更新说明(最好二者都通知)。
 

版本控制

必须对API进行版本控制。版本控制能够快速迭代并避免无效的请求访问已更新的接入点。它也有助于帮助平滑过渡任何大范围的API版本变迁,这样就能够继续支持旧版本API。

关于API的版本是否应该包含在URL或者请求头中 莫衷一是。从学术派的角度来说,它应该出如今请求头中。然而版本信息出如今URL中必须保证不一样版本资源的浏览器可浏览性(browser explorability),还记得文章开始提到的API要求吗?

我很是同意 approach that Stripe has taken to API versioning - URL包含一个主版本号(好比http://shonzilla/api/v1/customers/1234)
),可是API还包含基于日期的子版本(好比http://shonzilla/api/v1.2/customers/1234),能够经过配置 HTTP请求头来进行选择。这种状况下,主版本确保API结构整体稳定性,而子版本会考虑细微的变化(field deprecation、接入点变化等)。

API不可能彻底稳定。变动不可避免,重要的是变动是如何被控制的。维护良好的文档、公布将来数月的deprecation计划,这些对于不少API来讲都是一些可行的举措。它归根结底是看对于业界和API的潜在消费者是否合理。

结果过滤,排序和搜索

最好是尽可能保持基本资源URL的简洁性。 复杂结果过滤器、排序需求和高级搜索 (当限定在单一类型的资源时) ,都可以做为在基本URL之上的查询参数来轻松实现。下面让咱们更详细的看一下:

过滤: 对每个字段使用一个惟一查询参数,就能够实现过滤。 例如,当经过“/tickets”终端来请求一个票据列表时,你可能想要限定只要那些在售的票。这能够经过一个像 GET /tickets?state=open 这样的请求来实现。这里“state”是一个实现了过滤功能的查询参数。

排序: 跟过滤相似, 一个泛型参数排序能够被用来描述排序的规则. 为适应复杂排序需求,让排序参数采起逗号分隔的字段列表的形式,每个字段前均可能有一个负号来表示按降序排序。咱们看几个例子:

  • GET /tickets?sort=-priority - 获取票据列表,按优先级字段降序排序
  • GET /tickets?sort=-priority,created_at - 获取票据列表,按“priority”字段降序排序。在一个特定的优先级内,较早的票排在前面。

搜索: 有时基本的过滤不能知足需求,这时你就须要全文检索的力量。或许你已经在使用  ElasticSearch 或者其它基于  Lucene 的搜索技术。当全文检索被用做获取某种特定资源的资源实例的机制时, 它能够被暴露在API中,做为资源终端的查询参数,咱们叫它“q”。搜索类查询应当被直接交给搜索引擎,而且API的产出物应当具备一样的格式,以一个普通列表做为结果。

把这些组合在一块儿,咱们能够建立如下一些查询:

  • GET /tickets?sort=-updated_at - 获取最近更新的票
  • GET /tickets?state=closed&sort=-updated_at - 获取最近更新而且状态为关闭的票。
  • GET /tickets?q=return&state=open&sort=-priority,created_at - 获取优先级最高、最早建立的、状态为开放的票,而且票上有 'return' 字样。

通常查询的别名

为了使普通用户的API使用体验更加愉快, 考虑把条件集合包装进容易访问的RESTful 路径中。好比上面的,最近关闭的票的查询能够被包装成 GET /tickets/recently_closed

限制哪些字段由API返回

API的使用者并不老是须要一个资源的完整表示。选择返回字段的功能由来已久,它使得API使用者可以最小化网络阻塞,并加速他们对API的调用。

使用一个字段查询参数,它包含一个用逗号隔开的字段列表。例如,下列请求得到的信息将刚刚足够展现一个在售票的有序列表:

GET /tickets?fields=id,subject,customer_name,updated_at&state=open&sort=-updated_at

更新和建立应该返回一个资源描述

一个 PUT, POST 或者 PATCH 调用可能会对指定资源的某些字段形成更改,而这些字段本不在提供的参数之列 (例如: created_at 或 updated_at 这两个时间戳)。 为了防止API使用者为了获取更新后的资源而再次调用该API,应当使API把更新(或建立)后的资源做为response的一部分来返回。

以一个产生建立活动的 POST 操做为例, 使用一个 HTTP 201 状态代码 而后包含一个 Location header 来指向新生资源的URL。

 

你是否应该HATEOAS?

(译注:Hypermedia as the Engine of Application State (HATEOAS)超媒体做为应用程序状态引擎)

对于API消费方是否应该建立连接,或者是否应该将连接提供给API,有许多混杂的观点。RESTful的设计原则指定了HATEOAS ,大体说明了与某个端点的交互应该定义在元数据(metadata)之中,这个元数据与输出结果一同到达,并不基于其余地方的信息。

虽然web逐渐依照HATEOAS类型的原则运做(咱们打开一个网站首页并随着咱们看到的页面中的连接浏览),我不认为咱们已经准备好API的 HATEOAS了。当浏览一个网站的时候,决定点击哪一个连接是运行时作出的。然而,对于API,决定哪一个请求被发送是在写API集成代码时作出的,并非 运行时。这个决定能够移交到运行时吗?固然能够,不过顺着这条路没有太多好处,由于代码仍然不能不中断的处理重大的API变化。也就是说,我认为 HATEOAS作出了承诺,可是尚未准备好迎接它的黄金时间。为了彻底实现它的潜能,须要付出更多的努力去定义围绕着这些原则的标准和工具。

目前而言,最好假定用户已经访问过输出结果中的文档&包含资源标识符,而这些API消费方会在制做连接的时候用到。关注标识符有几个优点——网络中的数据流减小了,API消费方存储的数据也减小了(由于它们存储的是小的标识符而不是包含标识符的URLs)。

一样的,在URL中提供本文倡导的版本号,对于在一个很长时间内API消费方存储资源标识符(而不是URLs),它更有意义。总之,标识符相对版本是稳定的,可是表示这一点的URL却不是的!

 

只返回JSON

是时候在API中丢弃XML了。XML冗长,难以解析,很难读,他的数据模型和大部分编程语言的数据模型 不兼容,而他的可扩展性优点在你的主要需求是必须序列化一个内部数据进行输出展现时变得不相干。

我不打算对上述进行解释了,貌似诸如 (YouTube, Twitter 和 Box)之类的已经开始了去XML化.

给你一张google趋势图,比较XML API 和 JSON API的,供你参考:

可是,若是你的客户群包括大量的企业客户,你会发现本身不得不支持XML的方式。若是你必须这样,一个新问题出现了:

媒体类型是应该基于Accept头仍是基于URL呢 ? 为确保浏览器的浏览性,应该基于URL。这里最明智的选择是在端点URL后面附加 .json 或 .xml 的扩展.

字段名称书写格式的 snake_case vs camelCase

若是你在使用JSON (JavaScript Object Notation) 做为你的主要表示格式,正确的方法就是遵照JavaScript命名约定——对字段名称使用camelCase!若是你要走用各类语言建设客户端库的路 线,最好使用它们惯用的命名约定—— C# & Java 使用camelCase, python & ruby 使用snake_case。

深思:我一直认为snake_case比JavaScript的camelCase约定更容易阅读。我没有任何证据来支持个人直觉,直到如今,基于从2010年的camelCase 和 snake_case的眼动追踪研究 (PDF),snake_case比驼峰更容易阅读20%!这种阅读上的影响会影响API的可勘探性和文档中的示例。

许多流行的JSON API使用snake_case。我怀疑这是因为序列化库听从它们所使用的底层语言的命名约定。也许咱们须要有JSON序列库来处理命名约定转换。

 

缺省状况下确保漂亮的打印和支持gzip

一个提供空白符压缩输出的API,从浏览器中查看结果并不美观。虽然一些有序的查询参数(如 ?pretty=true )能够提供来使漂亮打印生效,一个默认状况下能进行漂亮打印的API更为平易近人。额外数据传输的成本是微不足道的,尤为是当你比较不执行gzip压缩的 成本。

考虑一些用例:假设分析一个API消费者正在调试而且有本身的代码来打印出从API收到的数据——默认状况下这应是可读的。或者,若是消费者抓住他们的代 码生成的URL,并直接从浏览器访问它——默认状况下这应是可读的。这些都是小事情。作好小事情会使一个API能被更愉快地使用!

那么该如何处理额外传输的数据呢?

让咱们看一个实际例子。我从GitHub API上拉取了一些数据,默认这些数据使用了漂亮打印(pretty print)。我也将作一些GZIP压缩后的对比。

$ curl https: //api .github.com /users/veesahni > with-whitespace.txt
$ ruby -r json -e 'puts JSON JSON.parse(STDIN.read)' < with-whitespace.txt > without-whitespace.txt
$ gzip -c with-whitespace.txt > with-whitespace.txt.gz
$ gzip -c without-whitespace.txt ? without-whitespace.txt.gz
 
输出文件的大小以下:
  • without-whitespace.txt - 1252 bytes
  • with-whitespace.txt - 1369 bytes
  • without-whitespace.txt.gz - 496 bytes
  • with-whitespace.txt.gz - 509 bytes

在这个例子中,当未启用GZIP压缩时空格增长了8.5%的额外输出大小,而当启用GZIP压缩时这个比例是2.6%。另外一方面,GZIP压缩节省了60%的带宽。因为漂亮打印的代价相对比较小,最好默认使用漂亮打印,并确保GZIP压缩被支持。

关于这点想了解更多的话,Twitter发现当对他们的 Streaming API 开启GZIP支持后能够在某些状况得到 80%的带宽节省 。Stack Exchange甚至强制要求必须对API请求结果使用GZIP压缩(never return a response that's not compressed)。 

 

不要默认使用大括号封装,但要在须要的时候支持

许多API会像下面这样包裹他们的响应信息:

{
   "data" : {
     "id" : 123,
     "name" : "John"
   }
}

有很多这样作的理由 - 更容易附加元数据或者分页信息,一些REST客户端不容许轻易的访问HTTP头信息,而且JSONP请求不能访问HTTP头信息。不管怎样,随着迅速被采用的标准,好比CORSLink header from RFC 5988, 大括号封装开始变得没必要要。

咱们应当默认不使用大括号封装,而仅在特殊状况下使用它,从而使咱们的API面向将来。

特殊状况下该如何使用大括号封装?

有两种状况确实须要大括号封装 - 当API须要经过JSONP来支持跨域的请求时,或者当客户端没有能力处理HTTP头信息时。

JSONP 请求附带有一个额外的查询参数(一般称为callback或jsonp) 表示了回调函数的名称。若是提供了这个参数,API应当切换至完整封装模式,这时它老是用200HTTP状态码做为响应,而后把真实的状态码放入JSON有效载荷中。任何被一并添加进响应中的额外的HTTP头信息都应当被映射到JSON字段中, 像这样:

callback_function({
   status_code: 200,
   next_page: "https://.." ,
   response: {
     ... actual JSON response body ...
   }
})
 
相似的,为了支持HTTP受限的客户端,能够容许一个特殊的查询参数“?envelope=true”来触发完整封装(没有JSONP回调函数)。
 

使用JSON 编码的 POST, PUT & PATCH 请求体

若是你正在跟随本文中讲述的开发过程,那么你确定已经接受JSON做为API的输出。下面让咱们考虑使用JSON做为API的输入。

许多API在他们的API请求体中使用URL编码。URL编码正如它们听起来那样 - 将使用和编码URL查询参数时同样的约定,对请求体中的键值对进行编码。这很简单,被普遍支持并且实用。

然而,有几个问题使得URL编码不太好用。首先,它没有数据类型的概念。这迫使API从字符串中转换整数和布尔值。并且,它并无真正的层次结构的概念。 尽管有一些约定,能够用键值对构造出一些结构(好比给一个键增长“[]”来表示一个数组),但仍是不能跟JSON原生的层次结构相比。

若是API很简单,URL编码能够知足须要。然而,复杂API应当严格对待他们的JSON格式的输入。不论哪一种方式,选定一个而且整套API要保持一致。

一个能接受JSON编码的POST, PUT 和 PATCH请求的API,应当也须要把Content-Type头信息设置为application/json,或者抛出一个415不支持的媒体类型(Unsupported Media Type)的HTTP状态码。

 

分页

信封喜欢将分页信息包含在信封自身的API。我不能指责这点——直到最近,咱们才找到更好的方法。正确的方法是使用 RFC 5988 中介绍的连接标头

使用连接标头的API能够返回一系列线程的连接,API使用者无需自行生成连接。这在分页时指针导向 很是重要。下面是抓取自 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"

不过这个并不是完成版本,由于不少 API 喜欢返回额外信息,例如可用结果的总数。须要发送数量的 API 可用相似 X-Total-Count 的普通 HTTP 标头。

 

自动装载相关的资源描述

在不少种状况下,API的使用者须要加载和被请求资源相关的数据(或被请求资源引用的数据)。与要求使用者反复访问API来获取这些信息相比,容许在请求原始资源的同时一并返回和装载相关资源,将会带来明显的效率提高。

然而, 因为这样确实 有悖于一些RESTful原则, 因此咱们能够只使用一个内置的(或扩展)的查询参数来实现这一功能,来最小化与原则的背离。

这种状况下,“embed”将是一个逗号隔开的须要被内置的字段列表。点号能够用来表示子字段。例如:

GET /ticket/12?embed=customer.name,assigned_user

这将返回一个附带有详细内置信息的票据,以下:

{
   "id" : 12,
   "subject" : "I have a question!" ,
   "summary" : "Hi, ...." ,
   "customer" : {
     "name" : "Bob"
   },
   assigned_user: {
    "id" : 42,
    "name" : "Jim" ,
   }
}
 
固然,实现相似于这种功能的能力彻底依赖于内在的复杂度。这种内置的作法很容易产生  N+1 select 问题
 

重写/覆盖   HTTP 方法

一些HTTP客户端仅能处理简单的的GET和POST请求,为照顾这些功能有限的客户端,API须要一种方式来重写HTTP方法. 尽管没有一些硬性标准来作这事,但流行的惯例是接受一种叫 X-HTTP的请求头,重写是用一个字符串值包含PUT,PATCH或DELETE中的一个。

注意重写头应当仅接受POST请求,GET请求毫不应该 更改服务器上的数据!

 

速率限制

为了防止滥用,标准的作法是给API增长某种类型的速率限制。RFC 6585 中介绍了一个HTTP状态码429 请求过多来实现这一点。

不论怎样,在用户实际受到限制以前告知他们限制的存在是颇有用的。这是一个如今还缺少标准的领域,可是已经有了一些流行的使用HTTP响应头信息的惯用方法。

最少时包含下列头信息(使用Twitter的命名约定 来做为头信息,一般没有中间词的大写):

  • X-Rate-Limit-Limit - 当期容许请求的次数
  • X-Rate-Limit-Remaining - 当期剩余的请求次数
  • X-Rate-Limit-Reset - 当期剩余的秒数

为何对X-Rate-Limit-Reset不使用时间戳而使用秒数?

一个时间戳包含了各类各样的信息,好比日期和时区,但它们却不是必需的。一个API使用者其实只是想知道何时能再次发起请求,对他们来讲一个秒数用最小的额外处理回答了这个问题。同时规避了时钟误差的问题。

有些API给X-Rate-Limit-Reset使用UNIX时间戳(纪元以来的秒数)。不要这样作!

为何对X-Rate-Limit-Reset使用UNIX时间戳是很差的作法?

HTTP 规范已经指定使用RFC 1123 的日期格式 (目前被使用在日期, If-Modified-Since & Last-Modified HTTP头信息中)。若是咱们打算指定一种使用某种形式时间戳的、新的HTTP头信息,咱们应当遵循RFC 1123规定,而不是使用UNIX时间戳。

 

认证

一个 RESTful API 应当是无状态的。这意味着认证请求应当不依赖于cookie或session。相反,每个请求都应当携带某种类型的认证凭证。

因为老是使用SSL,认证凭证可以被简化为一个随机产生的访问令牌,里面传入一个使用HTTP Basic Auth的用户名字段。这样作的极大的好处是,它是彻底的浏览器可探测的 - 若是浏览器从服务器收到一个401未受权状态码,它仅须要一个弹出框来索要凭证便可。

然而,这种基于基本认证的令牌的认证方法,仅在知足下列情形时才可用,即用户能够把令牌从一个管理接口复制到API使用者环境。当这种情形不能成立时,应当使用OAuth 2来产生安全令牌并传递给第三方。OAuth 2使用了承载令牌(Bearer tokens) 而且依赖于SSL的底层传输加密。

一个须要支持JSONP的API将须要第三种认证方法,由于JSONP请求不能发送HTTP基本认证凭据(HTTP Basic Auth)或承载令牌(Bearer tokens) 。这种状况下,可使用一个特殊的查询参数access_token。注意,使用查询参数token存在着一个固有的安全问题,即大多数的web服务器都会把查询参数记录到服务日志中。

这是值得的,全部上面三种方法都只是跨API边界两端的传递令牌的方式。实际的底层令牌自己可能都是相同的。

 

缓存

HTTP 提供了一套内置的缓存框架! 全部你必须作的是,包含一些额外的出站响应头信息,而且在收到一些入站请求头信息时作一点儿校验工做。

有两种方式: ETagLast-Modified

ETag: 当产生一个请求时, 包含一个HTTP 头,ETag会在里面置入一个和表达内容对应的哈希值或校验值。这个值应当跟随表达内容的变化而变化。如今,若是一个入站HTTP请求包含了一个If-None-Match头和一个匹配的ETag值,API应当返回一个304未修改状态码,而不是返回请求的资源。

Last-Modified: 基本上像ETag那样工做,不一样的是它使用时间戳。在响应头中,Last-Modified包含了一个RFC 1123格式的时间戳,它使用If-Modified-Since来进行验证。注意,HTTP规范已经有了 3 种不一样的可接受的日期格式 ,服务器应当准备好接收其中的任何一种。

错误

就像一个HTML错误页面给访问者展现了有用的错误信息同样,一个API应当以一种已知的可以使用的格式来提供有用的错误信息。 错误的表示形式应当和其它任何资源没有区别,只是有一套本身的字段。

API应当老是返回有意义的HTTP状态代码。API错误一般被分红两种类型: 表明客户端问题的400系列状态码和表明服务器问题的500系列状态码。最简状况下,API应当把便于使用的JSON格式做为400系列错误的标准化表 示。若是可能(意思是,若是负载均衡和反向代理能建立自定义的错误实体), 这也适用于500系列错误代码。

 

一个JSON格式的错误信息体应当为开发者提供几样东西 - 一个有用的错误信息,一个惟一的错误代码 (可以用来在文档中查询详细的错误信息) 和可能的详细描述。这样一个JSON格式的输出可能会像下面这样:

{
   "code" : 1234,
   "message" : "Something bad happened :(" ,
   "description" : "More details about the error here"
}
对PUT, PATCH和POST请求进行错误验证将须要一个字段分解。下面多是最好的模式:使用一个固定的顶层错误代码来验证错误,并在额外的字段中提供详细错误信息,就像这样:
{
   "code" : 1024,
   "message" : "Validation Failed" ,
   "errors" : [
     {
       "code" : 5432,
       "field" : "first_name" ,
       "message" : "First name cannot have fancy characters"
     },
     {
        "code" : 5622,
        "field" : "password" ,
        "message" : "Password cannot be blank"
     }
   ]
}
 

HTTP 状态代码

HTTP定义了一套能够从API返回的有意义的状态代码。 这些代码可以用来帮助API使用者对不一样的响应作出相应处理。我已经把你必然会用到的那些列成了一个简短的清单:

  • 200 OK (成功) - 对一次成功的GET, PUT, PATCH 或 DELETE的响应。也可以用于一次未产生建立活动的POST
  • 201 Created (已建立) - 对一次致使建立活动的POST的响应。 同时结合使用一个位置头信息指向新资源的位置- Response to a POST that results in a creation. Should be combined with a Location header pointing to the location of the new resource
  • 204 No Content (没有内容) - 对一次没有返回主体信息(像一次DELETE请求)的请求的响应
  • 304 Not Modified (未修改) - 当使用HTTP缓存头信息时使用304
  • 400 Bad Request (错误的请求) - 请求是畸形的, 好比没法解析请求体
  • 401 Unauthorized (未受权) - 当没有提供或提供了无效认证细节时。若是从浏览器使用API,也能够用来触发弹出一次认证请求
  • 403 Forbidden (禁止访问) - 当认证成功可是认证用户无权访问该资源时
  • 404 Not Found (未找到) - 当一个不存在的资源被请求时
  • 405 Method Not Allowed (方法被禁止) - 当一个对认证用户禁止的HTTP方法被请求时
  • 410 Gone (已删除) - 表示资源在终端再也不可用。当访问老版本API时,做为一个通用响应颇有用
  • 415 Unsupported Media Type (不支持的媒体类型) - 若是请求中包含了不正确的内容类型
  • 422 Unprocessable Entity (没法处理的实体) - 出现验证错误时使用
  • 429 Too Many Requests (请求过多) - 当请求因为访问速率限制而被拒绝时

总结

一个API是一个给开发者使用的用户接口。要努力确保它不只功能上可用,更要用起来愉快。