11

英文原文: HTTP API Design Guide
本文译者: LeoXu, Garfielt, 无若, --zxp


介绍

本指南描述了一套有关 HTTP+JSON API 的设计实践, 原始内容提取自 Heroku 平台 API 的工作.

本指南是对API的补充,也是Heroku新的内部API的指南. 我们希望引起Heroku之外的API设计者的兴趣.

这里我们的目标是一致的,专注于业务逻辑而避免脱节的设计. 我们就是要寻找一个良好的,一致的,文档优良的方式来设计API,而没必要是唯一理想的方式.

我们假定你熟悉HTTP+JSON API的一些基础,不会再指南中涵盖所有基础性的东西.

我们欢迎为这一指南 做出贡献.

返回适当的状态码

对于每一种响应返回适当的HTTP状态码. 成功的响应应该根据下面的指南编码:

  • 200: GET调用请求成功, 以及DELETE 或者 PATCH 调用同步完成
  • 201: 同步完成的POST调用请求成功
  • 202: 请求接受一个将会被同步处理的POST,DELETE或者PATCH调用
  • 206: GET请求成功,但只有部分响应返回: 见 上述有关范围的内容

请阅读指导有关用户错误和服务器错误情况的状态码的HTTP 响应码文档

提供可用的完整资源

尽可能在响应中提供完整的资源描述 (例如,带有所有属性的对象). 总是在200和201响应中提供完整的资源, 包括 PUT/PATCH 和 DELETE 请求, 例如:

$ curl -X DELETE \  
  https://service.com/apps/1f9b/domains/0fd4

HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8
...
{
  "created_at": "2012-01-01T12:00:00Z",
  "hostname": "subdomain.example.com",
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "updated_at": "2012-01-01T12:00:00Z"
}

202 响应不会包含完整的资源描述,例如:

$ curl -X DELETE \  
  https://service.com/apps/1f9b/dynos/05bd

HTTP/1.1 202 Accepted
Content-Type: application/json;charset=utf-8
...
{}

接受请求中序列化的JSON

接受PUT/PATCH/POST请求中的序列化JSON, 作为表单编码数据的替代或者补充. 这样就可以创建对称的JSON序列化响应,例如:

$ curl -X POST https://service.com/apps \
    -H "Content-Type: application/json" \
    -d '{"name": "demoapp"}'

{
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "name": "demoapp",
  "owner": {
    "email": "username@example.com",
    "id": "01234567-89ab-cdef-0123-456789abcdef"
  },
  ...
}

提供资源 (UU)ID

默认给每一个资源都指定一个id. 除非你有更好的理由,不然就使用UUID. 不要使用在整个服务或者服务中其它资源那里不是全局唯一的ID, 特别是自增长的ID.

用小写 8-4-4-4-12 格式生成UUID,例如:

"id": "01234567-89ab-cdef-0123-456789abcdef"

提供标准的时间戳

默认为资源提供创建和更新的时间戳,例如:

{
  ...
  "created_at": "2012-01-01T12:00:00Z",
  "updated_at": "2012-01-01T13:00:00Z",
  ...}

这些时间戳可能对一些资源没啥用,如此则可以省去.

使用ISO8601中的UTC时间格式

只使用UTC接收和返回时间. 使用 ISO8601 格式来生成时间,例如:

"finished_at": "2012-01-01T12:00:00Z"

使用一致的路径格式

资源名称

使用资源名称的复数形式,除非系统中相关的资源是唯一的(例如,在大多数系统,用户的账户永远都只能有一个). 这就能在你引用特定的资源时保持一致的方式.

操作

首选端点布局,因为它不需要对单独的资源有任何特殊的操作. 有些情况下是需要特殊操作的,那就把它们放在一个标准的前缀下,以清楚的界定它们:

/resources/:resource/actions/:action

例如.

/runs/{run_id}/actions/stop

小写的路径和属性

使用小写和用虚线符号分隔的路径名,便于同主机名对齐, 例如:

service-api.com/users
service-api.com/app-setups

属性同样也使用小写,但是使用下划线做分隔,那就属性名在Javascript中就可以不用引号了, 例如:

service_class: "first"

内联外键关系

使用一个内联的对象来序列化外键引用,例如:

{
  "name": "service-production",
  "owner": {
    "id": "5d8201b0..."
  },
  ...}

而不是如下例:

{
  "name": "service-production",
  "owner_id": "5d8201b0...",
  ...}

这种方式使得在不必改变响应结构或者引入更多顶级响应域的前提下内联如更多相关资源的信息,例如:

{
  "name": "service-production",
  "owner": {
    "id": "5d8201b0...",
    "name": "Alice",
    "email": "alice@heroku.com"
  },
  ...}

支持为方便起见的非id间接引用

在某些情况下对于端用户而言提供一个ID标志一个资源可能会方便些。例如,一个用户会需要一个Heroku应用名称,但那个应用时用UUID标识的。在这些情况下你可能想要同时接受名称和ID,例如:

$ curl https://service.com/apps/{app_id_or_name}
$ curl https://service.com/apps/97addcf0-c182
$ curl https://service.com/apps/www-prod

不要只接受名称而排斥ID.

生成结构性的错误

使用一致的,结构化的错误响应. 包括一个依赖于机器的错误id,一个人类可读的错误消息,以及可选的一个指出有关该错误及如何解决的信息的url,例如:

HTTP/1.1 429 Too Many Requests
{
  "id":      "rate_limit",
  "message": "Account reached its API rate limit.",
  "url":     "https://docs.service.com/rate-limits"}

为你的错误格式,以及客户端可能会遇到的错误id编写文档.

支持使用Etag的缓存

在所有的响应中包含一个ETag头,以标识返回资源的特定版本. 用户就能够从If-None-Match头获取的值中检查出他们的后续请求的是否已经过时.

使用Request-Id跟踪请求

在每一个API响应中包含一个Request-Id头,填充一个UUID值。如果服务器和客户端都记录了这个值的话,它就能在跟踪和调试请求方面起到作用.

使用范围进行分页

对容易产生大量数据的响应进行分页. 使用 Content-Range 头来传送分页请求. 详细的可以看看 Heroku 平台有关范围的API 中的请求和响应头, 状态码, 限制,排序和分页浏览的示例.

展示速率限制状态

来自客户端的速率限制请求用以保护服务的健康,并为其它的客户端保持较高的服务质量. 你可以使用一种 令牌桶算法 来量化请求限制.

可以在RateLimit-Remaining响应头中返回每个请求的剩余请求令牌数量.

带有版本的接收头

从一开始就要对API进行版本话。使用接收头,以及一个自定义内容类型来同版本进行交互,例如:

Accept: application/vnd.heroku+json; version=3

不去指定一个默认的版本,而不是要求客户端明确指定它们要使用一个特定的版本.

最小化路径内联

在带有内联父/子资源关系的数据模型中,路径可能会内联得很深,例如:

/orgs/{org_id}/apps/{app_id}/dynos/{dyno_id}

可以通过在根路径定位资源来限制内联深度. 使用内联来指定范围集合.例如,上述情况中一个dyno就属于一个属于org的app:

/orgs/{org_id}
/orgs/{org_id}/apps
/apps/{app_id}
/apps/{app_id}/dynos
/dynos/{dyno_id}

提供机器可读的JSON模式

提供一个机器可读的模式可以精确的指定你的API。使用 prmd 来管理你的模式,并确保它能被prmd verify验证.

提供人类可读的文档

提供客户端开发者可以用来理解你的API的人类可读文档.

如果你使用prmd创建了一个如上所述的模式,那么你就可以很容易的使用prmd doc来为所有的端点生成Markdown文档.

除了端点的详细信息之外,还要提供API概述的一些信息:

  • 认证,包括获取和使用认证令牌.
  • API 稳定性和版本,包括如何选择理想的API版本.
  • 通用的请求和响应头.
  • 错误的序列化格式.
  • 用不同的语言使用API的客户端的示例.

提供可执行的示例

提供可执行的示例,用户可以直接在终端中敲入命令来查看API的调用如何运行. 为了尽可能的扩展,这些示例应该要可以照字面意义使用,以最小化用户尝试这些API所需要做的事情,例如:

$ export TOKEN=... # acquire from dashboard
$ curl -is https://$TOKEN@service.com/users

如果你使用 prmd 生成了Markdown文档,你就将可以不费力的获得每一个端点的示例.

稳定性描述

描述API的稳定性或者依据其成熟性和稳定性的各个点,例如:以原型/开发/成品作为标志节点。

查看Heroku API兼容性策略为稳定性和变更管理方法提供一种可能。

一旦你的API被定义为是为生产所准备的和坚固的,那当API版本改变的时候,要使得这些API能有向后的兼容性。在创建一个新的API时,如果你需要做向后不兼容的变更,应增加版本号。

SSL需求

无一例外,要SSL去访问API时,无论用不用SSL,都不必找出以及解释其原因,它们就是需要SSL。

良好打印的默认json

用户第一次查看你的api很可能是在使用curl的命令行里。如果API的响应有良好的打印格式,那在命令行里它们会很容易理解。为了给这些开发者提供方便,良好打印格式的JSON如下:

{
  "beta": false,
  "email": "alice@heroku.com",
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "last_login": "2012-01-01T12:00:00Z",
  "created_at": "2012-01-01T12:00:00Z",
  "updated_at": "2012-01-01T12:00:00Z"}

而不是:

{"beta":false,"email":"alice@heroku.com","id":"01234567-89ab-cdef-0123-456789abcdef","last_login":"2012-01-01T12:00:00Z", "created_at":"2012-01-01T12:00:00Z","updated_at":"2012-01-01T12:00:00Z"}

要确保在JSON结尾有换行,以防止阻塞用户的终端界面。

对于大部分API的响应,性能考滤要优先于良好打印。在某些结点(例如高流量结点)或为某些特定用户(例如无GUI界面的程序)使用时,你可能会考滤使用高性能而非良好打印的API。

注:headless program译为“无显示界面的程序”,参考自这篇文章.


justjavac
47.8k 声望15.9k 粉丝

会写点 js 代码