根据 Heroku Platform API 的设计经验总结而来的 HTTP API 设计指南
Pull request Compare This branch is 8 commits ahead, 61 commits behind interagent:master.
Permalink
Failed to load latest commit information.
CONTRIBUTING.md init commit Sep 1, 2014
CONTRIBUTORS.md init commit Sep 1, 2014
LICENSE.md init commit Sep 1, 2014
README.md merge from the English version Sep 22, 2014

README.md

HTTP API 设计指南

概述

该指南讲解了一系列 HTTP+JSON API 设计经验。这些经验最初来自 Heroku 平台 API 的实践。

该指南对此 API 进行了补充,并且对 Heroku 的新的内部 API 起到了指导作用。 我们希望在 Heroku 之外的 API 设计者也会对此感兴趣。

本文的目标是在保持一致性,且关注业务逻辑的同时,避免设计歧义。我们一直在寻找 一种良好的、一致的、文档化的方法来设计 API,但没必要是唯一的/理想化的方法

本文假设读者已经对 HTTP+JSON API 的基本知识有所了解, 因此不会在指南中涵盖所有的基础概念。

欢迎对该指南给与贡献

目录

基础

必须使用 TLS

必须使用 TLS 来访问 API,没有例外。任何试图阐明或解释什么时候用它合适, 什么时候用它不合适都是徒劳。让任何请求都需要使用 TLS。

理想情况下,为了避免任何不安全的数据交换,对任何 HTTP 或端口 80 的非 TLS 的请求都应当不进行响应。 实际环境中,这不太可能,所以需要响应 403 Forbidden

由于马虎的/恶意的客户端行为无法提供任何明确的保障,所以不建议使用重定向。 重定向的客户端使得服务器的流量成倍增长,并且会在第一次调用的时候让敏感的数据暴露出来,使得 TLS 不起作用。

用 Accept 头指定版本

从一开始就对 API 添加版本。使用 Accept 头和自定义的内容类型来指定版本,例如:

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

最好不要用默认的版本,让客户端明确指出它们需要使用的版本。

利用 Etag 支持缓存

在所有响应中包含 ETag 头,用以标识返回资源的特定版本。 用户应当可以在随后的请求中,通过在 If-None-Match 头中指定该值来检查过期。

通过 Request-Id 跟踪请求

在每个 API 响应中包含 Request-Id 头,并附加一个 UUID 值。 如果服务器和客户端都对该值进行了记录,那么在跟踪和调试请求的时候会非常有用。

使用 Content-Range 进行分页

对任何响应都进行分页,使得大量数据容易被处理。 使用 Content-Range 头来传递分页请求。参阅 Heroku Platform API on Ranges 中的例子来了解请求和响应的头、状态码、上限、排序和跳转的细节。

请求

返回适当的状态码

对每一个请求都返回适当的 HTTP 状态码。根据本指南,成功的响应当使用以下代码:

  • 200: 对于 GET 以及完全同步的 DELETEPATCH 的请求成功时
  • 201: 对于完全同步的 POST 请求成功时
  • 202: 对于异步的 POSTDELETEPATCH 请求被接受
  • 206: GET 请求成功,不过只有部分内容被返回:参阅前面关于分页的内容

在使用身份验证与身份验证错误码时务必当心:

  • 401 Unauthorized: 由于用户未进行身份验证,所以请求失败
  • 403 Forbidden: 由于用户无权对特定资源进行访问,所以请求失败

当遇到错误的时候,需要返回合适的代码里提供附加的信息:

  • 422 Unprocessable Entity: 请求可以被解析,但包含了错误的参数
  • 429 Too Many Requests: 请求达到频度限制,稍候再试
  • 500 Internal Server Error: 服务器发生了一些错误,检查状态站点或提交一个 issue

参阅 HTTP response code spec 了解用户错误与服务器错误的情况下的状态码。

尽可能提供完整的资源

在可能的情况下,在响应中提供完整的资源(例如对象和其所有属性)。 在 200 和 201 响应中提供完整的资源,包括 PUT/PATCHDELETE 请求,例如:

$ 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"
  },
  ...
}

使用一致的路径格式

资源名

使用附带版本的资源名称,除非该资源在系统中仅有一个实例(例如,在大多数系统里,一个给定的用户只能有一个账户)。 这与引用特定资源的方法一致。

操作

对于个别无须特定操作的资源,宁可使用直接的布局。而需要特定操作的情况下, 将其放置在标准的 actions 前缀后,来描述它们:

/resources/:resource/actions/:action

例如:

/runs/{run_id}/actions/stop

小写的路径和属性

使用小写的、横线分隔的路径名称,与主机名一致,例如:

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

属性也小写,但是使用下划线分隔,这样属性名在 JavaScript 里无须转义,例如:

service_class: "first"

为了方便支持非 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 排除在外。

最少的路径嵌套

在数据模型中有着父子嵌套关系的资源,路径可能会深层嵌套,例如:

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

限制嵌套的深度,让资源相对于根路径来定位。使用嵌套来表示域集合。 例如,上面的例子中 dyno 属于一个 app,app 属于一个 org:

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

响应

为资源提供 (UU)ID

给每个资源一个默认的 id 属性。除非有一个好理由,否则还是使用 UUID 吧。 不要使用那些在跨服务器实例或服务的其他资源中不是全局唯一的 ID,特别是不要使用自增 ID。

将 UUID 定义为小写的 8-4-4-4-12 格式,例如:

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

提供标准的时间戳

为资源默认提供 created_atupdated_at 时间戳,例如:

{
  ...
  "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"

嵌套的外键关系

用嵌套的对象来表达外键关系,例如:

{
  "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,人工可读的错误 信息, 以及可选的 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 编写文档。

显示请求频度限制的状态

限制客户端的请求频度可以保护服务,并保持其他客户端较高的服务质量。可以使用 token bucket algorithm 来验证请求的频度。

在每个请求里都用 RateLimit-Remaining 响应头返回请求 token 的剩余请求数。

在所有请求中都保持 JSON 简洁

额外的空白字符会增加响应的大小,这是不必要的,而许多人工的客户端都会自动“美化” JSON 的输出。 所以最好让 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"
}

也可以考虑为客户端增加可选的方式来输出更详细的响应,不论是通过请求参数(例如 ?pretty=true) 或者通过 Accept 头参数(例如 Accept: application/vnd.heroku+json; version=3; indent=4;)。

辅助

提供机器可识别的 JSON schema

提供机器可识别的 schema 来明确你的 API。使用 prmd 来管理这些模式,并用 prmd verify 来验证。

提供可读的文档

提供可读的文档来让客户端开发者了解你的 API。

如果用上面提到的 prmd 创建了一个 schema,就可以很容易的通过 prmd doc 为所有接口创建 Markdown 文档。

作为接口的附加细节,为 API 提供以下信息的概述:

  • 身份验证,包括获得和使用身份验证 token;
  • API 的稳定程度与版本状况,包括如何选择目标版本的 API;
  • 通用的请求和响应头;
  • 错误的格式;
  • 不同客户端语言的使用示例。

提供可执行的例子

提供用户可以直接在终端中输入来了解 API 调用情况的可执行的例子。 为了最大程度的可扩展性,这些例子应当每行都可以使用, 以降低用户尝试这些 API 的工作量,例如:

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

如果你使用 prmd 来生成 Markdown 文档, 你可以很容易的获得每个接口的例子。

对稳定度进行描述

对你的 API 的稳定程度进行描述,包括不同接口的成熟度和稳定度。 例如,使用 prototype/development/production 标识。

参阅 Heroku API compatibility policy 了解可能的稳定度和变更管理的方法。

一旦 API 被定义为生产环境适用且为稳定的,就不要对那个版本的 API 进行任何会破坏向后兼容性的改变。 如果需要进行向后不兼容的修改,创建一个具有更高版本号的新 API。