├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── LICENSE.md └── README.md /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## 贡献 2 | 3 | 我们欢迎任何关于本指南相关内容的讨论和贡献。如有任何修改建议,请在本版本库建立 issue 或 pull request。 4 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | ## 贡献人 2 | 3 | (_英文_) 4 | 5 | * Wesley Beary 6 | * Mark McGranaghan 7 | * Jamu Kakar 8 | * Jonathan Roes 9 | 10 | (_中文_) 11 | 12 | * Xing Xing 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## 许可 2 | 3 | [项目贡献人](CONTRIBUTORS.md)版权所有。 4 | 5 | 在 [Creative Commons Attribution 3.0 Unported License](http://creativecommons.org/licenses/by/3.0/) 下发布。 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTTP API 设计指南 2 | 3 | ## 概述 4 | 5 | 该指南讲解了一系列 HTTP+JSON API 设计经验。这些经验最初来自 6 | [Heroku 平台 API](https://devcenter.heroku.com/articles/platform-api-reference) 7 | 的实践。 8 | 9 | 该指南对此 API 进行了补充,并且对 Heroku 的新的内部 API 起到了指导作用。 10 | 我们希望在 Heroku 之外的 API 设计者也会对此感兴趣。 11 | 12 | 本文的目标是在保持一致性,且关注业务逻辑的同时,避免设计歧义。我们一直在寻找 13 | _一种良好的、一致的、文档化的方法_来设计 API,但没必要是_唯一的/理想化的方法_。 14 | 15 | 本文假设读者已经对 HTTP+JSON API 的基本知识有所了解, 16 | 因此不会在指南中涵盖所有的基础概念。 17 | 18 | 欢迎对该指南给与[贡献](CONTRIBUTING.md)。 19 | 20 | ## 目录 21 | 22 | * [基础](#基础) 23 | * [必须使用 TLS](#必须使用-tls) 24 | * [用 Accept 头指定版本](#用-accept-头指定版本) 25 | * [利用 Etag 支持缓存](#利用-etag-支持缓存) 26 | * [通过 Request-Id 跟踪请求](#通过-request-id-跟踪请求) 27 | * [使用 Content-Range 进行分页](#使用-content-range-进行分页) 28 | * [请求](#请求) 29 | * [返回适当的状态码](#返回适当的状态码) 30 | * [尽可能提供完整的资源](#尽可能提供完整的资源) 31 | * [允许 JSON 编码的请求体](#允许-json-码的请求体) 32 | * [使用一致的路径格式](#使用一致的路径格式) 33 | * [小写的路径和属性](#小写的路径和属性) 34 | * [为了方便支持非 id 的引用](#为了方便支持非-id-的引用) 35 | * [最少的路径嵌套](#最少的路径嵌套) 36 | * [响应](#响应) 37 | * [为资源提供 (UU)ID](#为资源提供-uuid) 38 | * [提供标准的时间戳](#提供标准的时间戳) 39 | * [使用 ISO8601 格式化的 UTC 时间](#使用-iso8601-格式化的-utc-时间) 40 | * [嵌套的外键关系](#嵌套的外键关系) 41 | * [生成结构化的错误](#生成结构化的错误) 42 | * [显示请求频度限制的状态](#显示请求频度限制的状态) 43 | * [在所有请求中都保持 JSON 简洁](#在所有请求中都保持-json-简洁) 44 | * [辅助](#辅助) 45 | * [提供机器可识别的 JSON schema](#提供机器可识别的-json-schema) 46 | * [提供可读的文档](#提供可读的文档) 47 | * [提供可执行的例子](#提供可执行的例子) 48 | * [对稳定度进行描述](#对稳定度进行描述) 49 | 50 | ### 基础 51 | 52 | #### 必须使用 TLS 53 | 54 | 必须使用 TLS 来访问 API,没有例外。任何试图阐明或解释什么时候用它合适, 55 | 什么时候用它不合适都是徒劳。让任何请求都需要使用 TLS。 56 | 57 | 理想情况下,为了避免任何不安全的数据交换,对任何 HTTP 或端口 80 的非 TLS 的请求都应当不进行响应。 58 | 实际环境中,这不太可能,所以需要响应 `403 Forbidden`。 59 | 60 | 由于马虎的/恶意的客户端行为无法提供任何明确的保障,所以不建议使用重定向。 61 | 重定向的客户端使得服务器的流量成倍增长,并且会在第一次调用的时候让敏感的数据暴露出来,使得 TLS 不起作用。 62 | 63 | #### 用 Accept 头指定版本 64 | 65 | 从一开始就对 API 添加版本。使用 `Accept` 头和自定义的内容类型来指定版本,例如: 66 | 67 | ``` 68 | Accept: application/vnd.heroku+json; version=3 69 | ``` 70 | 71 | 最好不要用默认的版本,让客户端明确指出它们需要使用的版本。 72 | 73 | #### 利用 Etag 支持缓存 74 | 75 | 在所有响应中包含 `ETag` 头,用以标识返回资源的特定版本。 76 | 用户应当可以在随后的请求中,通过在 `If-None-Match` 头中指定该值来检查过期。 77 | 78 | #### 通过 Request-Id 跟踪请求 79 | 80 | 在每个 API 响应中包含 `Request-Id` 头,并附加一个 UUID 值。 81 | 如果服务器和客户端都对该值进行了记录,那么在跟踪和调试请求的时候会非常有用。 82 | 83 | #### 使用 Content-Range 进行分页 84 | 85 | 对任何响应都进行分页,使得大量数据容易被处理。 86 | 使用 `Content-Range` 头来传递分页请求。参阅 [Heroku Platform API on Ranges](https://devcenter.heroku.com/articles/platform-api-reference#ranges) 中的例子来了解请求和响应的头、状态码、上限、排序和跳转的细节。 87 | 88 | ### 请求 89 | 90 | #### 返回适当的状态码 91 | 92 | 对每一个请求都返回适当的 HTTP 状态码。根据本指南,成功的响应当使用以下代码: 93 | 94 | * `200`: 对于 `GET` 以及完全同步的 `DELETE` 或 `PATCH` 的请求成功时 95 | * `201`: 对于完全同步的 `POST` 请求成功时 96 | * `202`: 对于异步的 `POST`、`DELETE` 或 `PATCH` 请求被接受 97 | * `206`: `GET` 请求成功,不过只有部分内容被返回:参阅[前面关于分页的内容](#使用-content-range-进行分页) 98 | 99 | 在使用身份验证与身份验证错误码时务必当心: 100 | 101 | * `401 Unauthorized`: 由于用户未进行身份验证,所以请求失败 102 | * `403 Forbidden`: 由于用户无权对特定资源进行访问,所以请求失败 103 | 104 | 当遇到错误的时候,需要返回合适的代码里提供附加的信息: 105 | 106 | * `422 Unprocessable Entity`: 请求可以被解析,但包含了错误的参数 107 | * `429 Too Many Requests`: 请求达到频度限制,稍候再试 108 | * `500 Internal Server Error`: 服务器发生了一些错误,检查状态站点或提交一个 issue 109 | 110 | 参阅 [HTTP response code spec](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html) 111 | 了解用户错误与服务器错误的情况下的状态码。 112 | 113 | #### 尽可能提供完整的资源 114 | 115 | 在可能的情况下,在响应中提供完整的资源(例如对象和其所有属性)。 116 | 在 200 和 201 响应中提供完整的资源,包括 `PUT`/`PATCH` 和 `DELETE` 请求,例如: 117 | 118 | ``` 119 | $ curl -X DELETE \ 120 | https://service.com/apps/1f9b/domains/0fd4 121 | 122 | HTTP/1.1 200 OK 123 | Content-Type: application/json;charset=utf-8 124 | ... 125 | { 126 | "created_at": "2012-01-01T12:00:00Z", 127 | "hostname": "subdomain.example.com", 128 | "id": "01234567-89ab-cdef-0123-456789abcdef", 129 | "updated_at": "2012-01-01T12:00:00Z" 130 | } 131 | ``` 132 | 133 | 202 响应将不会包含完整的资源,例如: 134 | 135 | ``` 136 | $ curl -X DELETE \ 137 | https://service.com/apps/1f9b/dynos/05bd 138 | 139 | HTTP/1.1 202 Accepted 140 | Content-Type: application/json;charset=utf-8 141 | ... 142 | {} 143 | ``` 144 | 145 | #### 允许 JSON 编码的请求体 146 | 147 | 对于 `PUT`/`PATCH`/`POST` 允许使用 JSON 编码的请求体,可以看作是对表单数据的替换或补充。 148 | 这与 JSON 编码的响应体对称,例如: 149 | 150 | ``` 151 | $ curl -X POST https://service.com/apps \ 152 | -H "Content-Type: application/json" \ 153 | -d '{"name": "demoapp"}' 154 | 155 | { 156 | "id": "01234567-89ab-cdef-0123-456789abcdef", 157 | "name": "demoapp", 158 | "owner": { 159 | "email": "username@example.com", 160 | "id": "01234567-89ab-cdef-0123-456789abcdef" 161 | }, 162 | ... 163 | } 164 | ``` 165 | 166 | #### 使用一致的路径格式 167 | 168 | ##### 资源名 169 | 170 | 使用附带版本的资源名称,除非该资源在系统中仅有一个实例(例如,在大多数系统里,一个给定的用户只能有一个账户)。 171 | 这与引用特定资源的方法一致。 172 | 173 | ##### 操作 174 | 175 | 对于个别无须特定操作的资源,宁可使用直接的布局。而需要特定操作的情况下, 176 | 将其放置在标准的 `actions` 前缀后,来描述它们: 177 | 178 | ``` 179 | /resources/:resource/actions/:action 180 | ``` 181 | 例如: 182 | 183 | ``` 184 | /runs/{run_id}/actions/stop 185 | ``` 186 | 187 | #### 小写的路径和属性 188 | 189 | 使用小写的、横线分隔的路径名称,与主机名一致,例如: 190 | 191 | ``` 192 | service-api.com/users 193 | service-api.com/app-setups 194 | ``` 195 | 属性也小写,但是使用下划线分隔,这样属性名在 JavaScript 里无须转义,例如: 196 | 197 | ``` 198 | service_class: "first" 199 | ``` 200 | 201 | #### 为了方便支持非 id 的引用 202 | 203 | 在某些情况下,让最终用户提供 ID 来标识一个资源可能不是那么方便。 204 | 例如,用户可能想的是 HeroKu 的应用名称,但是那个应用可能是用 UUID 标识的。 205 | 在这种情况里,可能需要同时接受 ID 和名称,例如: 206 | 207 | ``` 208 | $ curl https://service.com/apps/{app_id_or_name} 209 | $ curl https://service.com/apps/97addcf0-c182 210 | $ curl https://service.com/apps/www-prod 211 | ``` 212 | 不要仅接受名字,而将 ID 排除在外。 213 | 214 | #### 最少的路径嵌套 215 | 216 | 在数据模型中有着父子嵌套关系的资源,路径可能会深层嵌套,例如: 217 | 218 | ``` 219 | /orgs/{org_id}/apps/{app_id}/dynos/{dyno_id} 220 | ``` 221 | 限制嵌套的深度,让资源相对于根路径来定位。使用嵌套来表示域集合。 222 | 例如,上面的例子中 dyno 属于一个 app,app 属于一个 org: 223 | 224 | ``` 225 | /orgs/{org_id} 226 | /orgs/{org_id}/apps 227 | /apps/{app_id} 228 | /apps/{app_id}/dynos 229 | /dynos/{dyno_id} 230 | ``` 231 | 232 | ### 响应 233 | 234 | #### 为资源提供 (UU)ID 235 | 236 | 给每个资源一个默认的 `id` 属性。除非有一个好理由,否则还是使用 UUID 吧。 237 | 不要使用那些在跨服务器实例或服务的其他资源中不是全局唯一的 ID,特别是不要使用自增 ID。 238 | 239 | 将 UUID 定义为小写的 `8-4-4-4-12` 格式,例如: 240 | 241 | ``` 242 | "id": "01234567-89ab-cdef-0123-456789abcdef" 243 | ``` 244 | 245 | #### 提供标准的时间戳 246 | 247 | 为资源默认提供 `created_at` 和 `updated_at` 时间戳,例如: 248 | 249 | ```json 250 | { 251 | ... 252 | "created_at": "2012-01-01T12:00:00Z", 253 | "updated_at": "2012-01-01T13:00:00Z", 254 | ... 255 | } 256 | ``` 257 | 这些时间说对于某些资源来说可能没有实际意义,在这些情况下它们可以被省略。 258 | 259 | #### 使用 ISO8601 格式化的 UTC 时间 260 | 261 | 只使用 UTC 接收或返回时间。用 ISO8601 格式表达时间,例如: 262 | 263 | ``` 264 | "finished_at": "2012-01-01T12:00:00Z" 265 | ``` 266 | 267 | #### 嵌套的外键关系 268 | 269 | 用嵌套的对象来表达外键关系,例如: 270 | 271 | ```json 272 | { 273 | "name": "service-production", 274 | "owner": { 275 | "id": "5d8201b0..." 276 | }, 277 | ... 278 | } 279 | ``` 280 | 281 | 而不是: 282 | 283 | ```json 284 | { 285 | "name": "service-production", 286 | "owner_id": "5d8201b0...", 287 | ... 288 | } 289 | ``` 290 | 291 | 这一机制允许嵌入更多相关资源的信息,而无须修改响应的数据结构,或引入更多的顶级字段,例如: 292 | 293 | ```json 294 | { 295 | "name": "service-production", 296 | "owner": { 297 | "id": "5d8201b0...", 298 | "name": "Alice", 299 | "email": "alice@heroku.com" 300 | }, 301 | ... 302 | } 303 | ``` 304 | 305 | #### 生成结构化的错误 306 | 307 | 生成一致的、结构化的错误响应。包括机器可识别的错误 `id`,人工可读的错误 `信息`, 308 | 以及可选的 `url` 引导客户了解关于错误的更进一步的信息和解决方案,例如: 309 | 310 | ``` 311 | HTTP/1.1 429 Too Many Requests 312 | ``` 313 | 314 | ```json 315 | { 316 | "id": "rate_limit", 317 | "message": "Account reached its API rate limit.", 318 | "url": "https://docs.service.com/rate-limits" 319 | } 320 | ``` 321 | 对错误格式和客户端可能遇到的错误 `id` 编写文档。 322 | 323 | #### 显示请求频度限制的状态 324 | 325 | 限制客户端的请求频度可以保护服务,并保持其他客户端较高的服务质量。可以使用 326 | [token bucket algorithm](http://en.wikipedia.org/wiki/Token_bucket) 来验证请求的频度。 327 | 328 | 在每个请求里都用 `RateLimit-Remaining` 响应头返回请求 token 的剩余请求数。 329 | 330 | #### 在所有请求中都保持 JSON 简洁 331 | 332 | 额外的空白字符会增加响应的大小,这是不必要的,而许多人工的客户端都会自动“美化” JSON 的输出。 333 | 所以最好让 JSON 的响应保持最小,例如: 334 | 335 | ```json 336 | {"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"} 337 | ``` 338 | 339 | 而不是: 340 | 341 | ```json 342 | { 343 | "beta": false, 344 | "email": "alice@heroku.com", 345 | "id": "01234567-89ab-cdef-0123-456789abcdef", 346 | "last_login": "2012-01-01T12:00:00Z", 347 | "created_at": "2012-01-01T12:00:00Z", 348 | "updated_at": "2012-01-01T12:00:00Z" 349 | } 350 | ``` 351 | 也可以考虑为客户端增加可选的方式来输出更详细的响应,不论是通过请求参数(例如 `?pretty=true`) 352 | 或者通过 `Accept` 头参数(例如 `Accept: application/vnd.heroku+json; version=3; indent=4;`)。 353 | 354 | ### 辅助 355 | 356 | #### 提供机器可识别的 JSON schema 357 | 358 | 提供机器可识别的 schema 来明确你的 API。使用 [prmd](https://github.com/interagent/prmd) 359 | 来管理这些模式,并用 `prmd verify` 来验证。 360 | 361 | #### 提供可读的文档 362 | 363 | 提供可读的文档来让客户端开发者了解你的 API。 364 | 365 | 如果用上面提到的 prmd 创建了一个 schema,就可以很容易的通过 366 | `prmd doc` 为所有接口创建 Markdown 文档。 367 | 368 | 作为接口的附加细节,为 API 提供以下信息的概述: 369 | 370 | * 身份验证,包括获得和使用身份验证 token; 371 | * API 的稳定程度与版本状况,包括如何选择目标版本的 API; 372 | * 通用的请求和响应头; 373 | * 错误的格式; 374 | * 不同客户端语言的使用示例。 375 | 376 | #### 提供可执行的例子 377 | 378 | 提供用户可以直接在终端中输入来了解 API 调用情况的可执行的例子。 379 | 为了最大程度的可扩展性,这些例子应当每行都可以使用, 380 | 以降低用户尝试这些 API 的工作量,例如: 381 | 382 | ``` 383 | $ export TOKEN=... # acquire from dashboard 384 | $ curl -is https://$TOKEN@service.com/users 385 | ``` 386 | 387 | 如果你使用 [prmd](https://github.com/interagent/prmd) 来生成 Markdown 文档, 388 | 你可以很容易的获得每个接口的例子。 389 | 390 | #### 对稳定度进行描述 391 | 392 | 对你的 API 的稳定程度进行描述,包括不同接口的成熟度和稳定度。 393 | 例如,使用 prototype/development/production 标识。 394 | 395 | 参阅 [Heroku API compatibility policy](https://devcenter.heroku.com/articles/api-compatibility-policy) 396 | 了解可能的稳定度和变更管理的方法。 397 | 398 | 一旦 API 被定义为生产环境适用且为稳定的,就不要对那个版本的 API 进行任何会破坏向后兼容性的改变。 399 | 如果需要进行向后不兼容的修改,创建一个具有更高版本号的新 API。 400 | 401 | --------------------------------------------------------------------------------