├── LICENSE └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 godruoyi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## RESTful API 设计规范 2 | 3 | 该仓库整理了目前比较流行的 `RESTful api` 设计规范,为了方便讨论规范带来的问题及争议,现把该文档托管于 `Github`,欢迎大家补充!! 4 | 5 | ## Table of Contents 6 | 7 | * [RESTful API 设计规范](#restful-api-设计规范) 8 | * [关于「能愿动词」的使用](#关于能愿动词的使用) 9 | * [Protocol](#protocol) 10 | * [API Root URL](#api-root-url) 11 | * [Versioning](#versioning) 12 | * [在 URL 中嵌入版本编号](#在-url-中嵌入版本编号) 13 | * [通过媒体类型来指定版本信息](#通过媒体类型来指定版本信息) 14 | * [Endpoints](#endpoints) 15 | * [HTTP 动词](#http-动词) 16 | * [Filtering](#filtering) 17 | * [Authentication](#authentication) 18 | * [Response](#response) 19 | * [200 ok](#200-ok) 20 | * [201 Created](#201-created) 21 | * [202 Accepted](#202-accepted) 22 | * [204 No Content](#204-no-content) 23 | * [3xx 重定向](#3xx-重定向) 24 | * [400 Bad Request](#400-bad-request) 25 | * [401 Unauthorized](#401-unauthorized) 26 | * [403 Forbidden](#403-forbidden) 27 | * [404 Not Found](#404-not-found) 28 | * [405 Method Not Allowd](#405-method-not-allowd) 29 | * [406 Not Acceptable](#406-not-acceptable) 30 | * [408 Request Timeout](#408-request-timeout) 31 | * [409 Gonfilct](#409-gonfilct) 32 | * [410 Gone](#410-gone) 33 | * [413 Request Entity Too Large](#413-request-entity-too-large) 34 | * [414 Request-URI Too Long](#414-request-uri-too-long) 35 | * [415 Unsupported Media Type](#415-unsupported-media-type) 36 | * [429 Too Many Request](#429-too-many-request) 37 | * [500 Internal Server Error](#500-internal-server-error) 38 | * [503 Service Unavailable](#503-service-unavailable) 39 | * [版权声明](#版权声明) 40 | * [建议参考](#建议参考) 41 | * [LICENSE](#license) 42 | 43 | ## 关于「能愿动词」的使用 44 | 45 | 为了避免歧义,文档大量使用了「能愿动词」,对应的解释如下: 46 | 47 | * `必须 (MUST)`:绝对,严格遵循,请照做,无条件遵守; 48 | * `一定不可 (MUST NOT)`:禁令,严令禁止; 49 | * `应该 (SHOULD)` :强烈建议这样做,但是不强求; 50 | * `不该 (SHOULD NOT)`:强烈不建议这样做,但是不强求; 51 | * `可以 (MAY)` 和 `可选 (OPTIONAL)` :选择性高一点,在这个文档内,此词语使用较少; 52 | 53 | > 参见:[RFC 2119](http://www.ietf.org/rfc/rfc2119.txt) 54 | 55 | ## Protocol 56 | 57 | 客户端在通过 `API` 与后端服务通信的过程中,`应该` 使用 `HTTPS` 协议。 58 | 59 | ## API Root URL 60 | 61 | `API` 的根入口点应尽可能保持足够简单,这里有两个常见的 `URL` 根例子: 62 | 63 | * api.example.com/* 64 | * example.com/api/* 65 | 66 | > 如果你的应用很庞大或者你预计它将会变的很庞大,那 `应该` 将 `API` 放到子域下(`api.example.com`)。这种做法可以保持某些规模化上的灵活性。 67 | 68 | ## Versioning 69 | 70 | 所有的 `API` 必须保持向后兼容,你 `必须` 在引入新版本 `API` 的同时确保旧版本 `API` 仍然可用。所以 `应该` 为其提供版本支持。 71 | 72 | 目前比较常见的两种版本号形式: 73 | 74 | ### 在 URL 中嵌入版本编号 75 | 76 | ```bash 77 | api.example.com/v1/* 78 | ``` 79 | 80 | 这种做法是版本号直观、易于调试;另一种做法是,将版本号放在 `HTTP Header` 头中: 81 | 82 | ### 通过媒体类型来指定版本信息 83 | 84 | ```bash 85 | Accept: application/vnd.example.com.v1+json 86 | ``` 87 | 88 | 其中 `vnd` 表示 `Standards Tree` 标准树类型,有三个不同的树: `x`,`prs` 和 `vnd`。你使用的标准树需要取决于你开发的项目 89 | 90 | * 未注册的树(`x`)主要表示本地和私有环境 91 | * 私有树(`prs`)主要表示没有商业发布的项目 92 | * 供应商树(`vnd`)主要表示公开发布的项目 93 | 94 | > 后面几个参数依次为应用名称(一般为应用域名)、版本号、期望的返回格式。 95 | 96 | 至于具体把版本号放在什么地方,这个问题一直存在很大的争议,但由于我们大多数时间都在使用 `Laravel` 开发,`应该` 使用 [dingo/api](https://github.com/dingo/api) 来快速构建应用,它采用第二种方式来管理 `API` 版本,并且已集成了标准的 `HTTP Response`。 97 | 98 | ## Endpoints 99 | 100 | 端点就是指向特定资源或资源集合的 `URL`。在端点的设计中,你 `必须` 遵守下列约定: 101 | 102 | * URL 的命名 `必须` 全部小写 103 | * URL 中资源(`resource`)的命名 `必须` 是名词,并且 `必须` 是复数形式 104 | * `必须` 优先使用 `Restful` 类型的 URL 105 | * URL `必须` 是易读的 106 | * URL `一定不可` 暴露服务器架构 107 | 108 | > 至于 URL 是否必须使用连字符(`-`) 或下划线(`_`),不做硬性规定,但 `必须` 根据团队情况统一一种风格。 109 | 110 | 来看一个反例 111 | 112 | * https://api.example.com/getUserInfo?userid=1 113 | * https://api.example.com/getusers 114 | * https://api.example.com/sv/u 115 | * https://api.example.com/cgi-bin/users/get_user.php?userid=1 116 | 117 | 再来看一个正列 118 | 119 | * https://api.example.com/zoos 120 | * https://api.example.com/animals 121 | * https://api.example.com/zoos/{zoo}/animals 122 | * https://api.example.com/animal_types 123 | * https://api.example.com/employees 124 | 125 | ## HTTP 动词 126 | 127 | 对于资源的具体操作类型,由 `HTTP` 动词表示。常用的 `HTTP` 动词有下面五个(括号里是对应的 `SQL` 命令)。 128 | 129 | * GET(SELECT):从服务器取出资源(一项或多项)。 130 | * POST(CREATE):在服务器新建一个资源。 131 | * PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。 132 | * PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。 133 | * DELETE(DELETE):从服务器删除资源。 134 | 135 | 其中 136 | 137 | 1 删除资源 `必须` 用 `DELETE` 方法 138 | 2 创建新的资源 `必须` 使用 `POST` 方法 139 | 3 更新资源 `应该` 使用 `PUT` 方法 140 | 4 获取资源信息 `必须` 使用 `GET` 方法 141 | 142 | 针对每一个端点来说,下面列出所有可行的 `HTTP` 动词和端点的组合 143 | 144 | | 请求方法 | URL | 描述 | 145 | | ---------- | --- | --- | 146 | | GET | /zoos | 列出所有的动物园(ID和名称,不要太详细) | 147 | | POST | /zoos | 新增一个新的动物园 | 148 | | GET | /zoos/{zoo} | 获取指定动物园详情 | 149 | | PUT | /zoos/{zoo} | 更新指定动物园(整个对象) | 150 | | PATCH | /zoos/{zoo} | 更新动物园(部分对象) | 151 | | DELETE | /zoos/{zoo} | 删除指定动物园 | 152 | | GET | /zoos/{zoo}/animals | 检索指定动物园下的动物列表(ID和名称,不要太详细) | 153 | | GET | /animals | 列出所有动物(ID和名称)。 | 154 | | POST | /animals | 新增新的动物 | 155 | | GET | /animals/{animal} | 获取指定的动物详情 | 156 | | PUT | /animals/{animal} | 更新指定的动物(整个对象) | 157 | | PATCH | /animals/{animal} | 更新指定的动物(部分对象) | 158 | | GET | /animal_types | 获取所有动物类型(ID和名称,不要太详细) | 159 | | GET | /animal_types/{type} | 获取指定的动物类型详情 | 160 | | GET | /employees | 检索整个雇员列表 | 161 | | GET | /employees/{employee} | 检索指定特定的员工 | 162 | | GET | /zoos/{zoo}/employees | 检索在这个动物园工作的雇员的名单(身份证和姓名) | 163 | | POST | /employees | 新增指定新员工 | 164 | | POST | /zoos/{zoo}/employees | 在特定的动物园雇佣一名员工 | 165 | | DELETE | /zoos/{zoo}/employees/{employee} | 从某个动物园解雇一名员工 | 166 | 167 | > 超出 `Restful` 端点的,`应该` 模仿上表的方式来定义端点。 168 | 169 | ## Filtering 170 | 171 | > 如果记录数量很多,服务器不可能都将它们返回给用户。API `应该` 提供参数,过滤返回结果。下面是一些常见的参数。 172 | 173 | * ?limit=10:指定返回记录的数量 174 | * ?offset=10:指定返回记录的开始位置。 175 | * ?page=2&per_page=100:指定第几页,以及每页的记录数。 176 | * ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。 177 | * ?animal_type_id=1:指定筛选条件 178 | 179 | 所有 `URL` 参数 `必须` 是全小写,`必须` 使用下划线类型的参数形式。 180 | 181 | > 分页参数 `必须` 固定为 `page`、`per_page` 182 | 183 | 经常使用的、复杂的查询 `应该` 标签化,降低维护成本。如 184 | 185 | ```bash 186 | GET /trades?status=closed&sort=sortby=name&order=asc 187 | 188 | # 可为其定制快捷方式 189 | GET /trades/recently_closed 190 | ``` 191 | 192 | ## Authentication 193 | 194 | `应该` 使用 `OAuth2.0` 的方式为 API 调用者提供登录认证。`必须` 先通过登录接口获取 `Access Token` 后再通过该 `token` 调用需要身份认证的 `API`。 195 | 196 | Oauth 的端点设计示列 197 | 198 | * RFC 6749 /token 199 | * Twitter /oauth2/token 200 | * Fackbook /oauth/access_token 201 | * Google /o/oauth2/token 202 | * Github /login/oauth/access_token 203 | * Instagram /oauth/authorize 204 | 205 | 客户端在获得 `access token` 的同时 `必须` 在响应中包含一个名为 `expires_in` 的数据,它表示当前获得的 `token` 会在多少 `秒` 后失效。 206 | 207 | ```json 208 | { 209 | "access_token": "token....", 210 | "token_type": "Bearer", 211 | "expires_in": 3600 212 | } 213 | ``` 214 | 215 | 客户端在请求需要认证的 `API` 时,`必须` 在请求头 `Authorization` 中带上 `access_token`。 216 | 217 | ```bash 218 | Authorization: Bearer token... 219 | ``` 220 | 221 | 当超过指定的秒数后,`access token` 就会过期,再次用过期/或无效的 `token` 访问时,服务端 `应该` 返回 `invalid_token` 的错误或 `401` 错误码。 222 | 223 | ```http 224 | HTTP/1.1 401 Unauthorized 225 | Content-Type: application/json 226 | Cache-Control: no-store 227 | Pragma: no-cache 228 | 229 | { 230 | "error": "invalid_token" 231 | } 232 | ``` 233 | 234 | > Laravel 开发中,`应该` 使用 [JWT](https://github.com/tymondesigns/jwt-auth) 来为管理你的 Token,并且 `一定不可` 在 `api` 中间件中开启请求 `session`。 235 | 236 | ## Response 237 | 238 | 所有的 `API` 响应,`必须` 遵守 `HTTP` 设计规范,`必须` 选择合适的 `HTTP` 状态码。`一定不可` 所有接口都返回状态码为 `200` 的 `HTTP` 响应,如: 239 | 240 | ```http 241 | HTTP/1.1 200 ok 242 | Content-Type: application/json 243 | Server: example.com 244 | 245 | { 246 | "code": 0, 247 | "msg": "success", 248 | "data": { 249 | "username": "username" 250 | } 251 | } 252 | ``` 253 | 254 | 或 255 | 256 | ```http 257 | HTTP/1.1 200 ok 258 | Content-Type: application/json 259 | Server: example.com 260 | 261 | { 262 | "code": -1, 263 | "msg": "该活动不存在", 264 | } 265 | ``` 266 | 267 | 下表列举了常见的 `HTTP` 状态码 268 | 269 | | 状态码 | 描述 | 270 | | ---------- | --- | 271 | | 1xx | 代表请求已被接受,需要继续处理 | 272 | | 2xx | 请求已成功,请求所希望的响应头或数据体将随此响应返回 | 273 | | 3xx | 重定向 | 274 | | 4xx | 客户端原因引起的错误 | 275 | | 5xx | 服务端原因引起的错误 | 276 | 277 | > 只有来自客户端的请求被正确的处理后才能返回 `2xx` 的响应,所以当 API 返回 `2xx` 类型的状态码时,前端 `必须` 认定该请求已处理成功。 278 | 279 | 必须强调的是,所有 `API` `一定不可` 返回 `1xx` 类型的状态码。当 `API` 发生错误时,`必须` 返回出错时的详细信息。目前常见返回错误信息的方法有两种: 280 | 281 | 1、将错误详细放入 `HTTP` 响应首部; 282 | 283 | ```http 284 | X-MYNAME-ERROR-CODE: 4001 285 | X-MYNAME-ERROR-MESSAGE: Bad authentication token 286 | X-MYNAME-ERROR-INFO: http://docs.example.com/api/v1/authentication 287 | ``` 288 | 289 | 2、直接放入响应实体中; 290 | 291 | ```http 292 | HTTP/1.1 401 Unauthorized 293 | Server: nginx/1.11.9 294 | Content-Type: application/json 295 | Transfer-Encoding: chunked 296 | Cache-Control: no-cache, private 297 | Date: Sun, 24 Jun 2018 10:02:59 GMT 298 | Connection: keep-alive 299 | 300 | {"error_code":40100,"message":"Unauthorized"} 301 | ``` 302 | 303 | 考虑到易读性和客户端的易处理性,我们 `必须` 把错误信息直接放到响应实体中,并且错误格式 `应该` 满足如下格式: 304 | 305 | ```json 306 | { 307 | "message": "您查找的资源不存在", 308 | "error_code": 404001 309 | } 310 | ``` 311 | 312 | 其中错误码(`error_code`)`必须` 和 `HTTP` 状态码对应,也方便错误码归类,如: 313 | 314 | ```http 315 | HTTP/1.1 429 Too Many Requests 316 | Server: nginx/1.11.9 317 | Content-Type: application/json 318 | Transfer-Encoding: chunked 319 | Cache-Control: no-cache, private 320 | Date: Sun, 24 Jun 2018 10:15:52 GMT 321 | Connection: keep-alive 322 | 323 | {"error_code":429001,"message":"你操作太频繁了"} 324 | ``` 325 | 326 | ```http 327 | HTTP/1.1 403 Forbidden 328 | Server: nginx/1.11.9 329 | Content-Type: application/json 330 | Transfer-Encoding: chunked 331 | Cache-Control: no-cache, private 332 | Date: Sun, 24 Jun 2018 10:19:27 GMT 333 | Connection: keep-alive 334 | 335 | {"error_code":403002,"message":"用户已禁用"} 336 | ``` 337 | 338 | `应该` 在返回的错误信息中,同时包含面向开发者和面向用户的提示信息,前者可方便开发人员调试,后者可直接展示给终端用户查看如: 339 | 340 | ```json 341 | { 342 | "message": "直接展示给终端用户的错误信息", 343 | "error_code": "业务错误码", 344 | "error": "供开发者查看的错误信息", 345 | "debug": [ 346 | "错误堆栈,必须开启 debug 才存在" 347 | ] 348 | } 349 | ``` 350 | 351 | 下面详细列举了各种情况 API 的返回说明。 352 | 353 | ### 200 ok 354 | 355 | `200` 状态码是最常见的 `HTTP` 状态码,在所有 **成功** 的 `GET` 请求中,`必须` 返回此状态码。`HTTP` 响应实体部分 `必须` 直接就是数据,不要做多余的包装。 356 | 357 | 错误示例: 358 | 359 | ```http 360 | HTTP/1.1 200 ok 361 | Content-Type: application/json 362 | Server: example.com 363 | 364 | { 365 | "user": { 366 | "id":1, 367 | "nickname":"fwest", 368 | "username": "example" 369 | } 370 | } 371 | ``` 372 | 373 | 正确示例: 374 | 375 | 1、获取单个资源详情 376 | 377 | ```json 378 | { 379 | "id": 1, 380 | "username": "godruoyi", 381 | "age": 88, 382 | } 383 | ``` 384 | 385 | 2、获取资源集合 386 | 387 | ```json 388 | [ 389 | { 390 | "id": 1, 391 | "username": "godruoyi", 392 | "age": 88, 393 | }, 394 | { 395 | "id": 2, 396 | "username": "foo", 397 | "age": 88, 398 | } 399 | ] 400 | ``` 401 | 402 | 3、额外的媒体信息 403 | 404 | ```json 405 | { 406 | "data": [ 407 | { 408 | "id": 1, 409 | "avatar": "https://lorempixel.com/640/480/?32556", 410 | "nickname": "fwest", 411 | "last_logined_time": "2018-05-29 04:56:43", 412 | "has_registed": true 413 | }, 414 | { 415 | "id": 2, 416 | "avatar": "https://lorempixel.com/640/480/?86144", 417 | "nickname": "zschowalter", 418 | "last_logined_time": "2018-06-16 15:18:34", 419 | "has_registed": true 420 | } 421 | ], 422 | "meta": { 423 | "pagination": { 424 | "total": 101, 425 | "count": 2, 426 | "per_page": 2, 427 | "current_page": 1, 428 | "total_pages": 51, 429 | "links": { 430 | "next": "http://api.example.com?page=2" 431 | } 432 | } 433 | } 434 | } 435 | ``` 436 | 437 | > 其中,分页和其他额外的媒体信息,必须放到 `meta` 字段中。 438 | 439 | ### 201 Created 440 | 441 | 当服务器创建数据成功时,`应该` 返回此状态码。常见的应用场景是使用 `POST` 提交用户信息,如: 442 | 443 | * 添加了新用户 444 | * 上传了图片 445 | * 创建了新活动 446 | 447 | 等,都可以返回 `201` 状态码。需要注意的是,你可以选择在用户创建成功后返回新用户的数据 448 | 449 | ```http 450 | HTTP/1.1 201 Created 451 | Server: nginx/1.11.9 452 | Content-Type: application/json 453 | Transfer-Encoding: chunked 454 | Date: Sun, 24 Jun 2018 09:13:40 GMT 455 | Connection: keep-alive 456 | 457 | { 458 | "id": 1, 459 | "avatar": "https:\/\/lorempixel.com\/640\/480\/?32556", 460 | "nickname": "fwest", 461 | "last_logined_time": "2018-05-29 04:56:43", 462 | "created_at": "2018-06-16 17:55:55", 463 | "updated_at": "2018-06-16 17:55:55" 464 | } 465 | ``` 466 | 467 | 也可以返回一个响应实体为空的 `HTTP Response` 如: 468 | 469 | ```http 470 | HTTP/1.1 201 Created 471 | Server: nginx/1.11.9 472 | Content-Type: text/html; charset=UTF-8 473 | Transfer-Encoding: chunked 474 | Date: Sun, 24 Jun 2018 09:12:20 GMT 475 | Connection: keep-alive 476 | ``` 477 | 478 | > 这里我们 `应该` 采用第二种方式,因为大多数情况下,客户端只需要知道该请求操作成功与否,并不需要返回新资源的信息。 479 | 480 | ### 202 Accepted 481 | 482 | 该状态码表示服务器已经接受到了来自客户端的请求,但还未开始处理。常用短信发送、邮件通知、模板消息推送等这类很耗时需要队列支持的场景中; 483 | 484 | > 返回该状态码时,响应实体 `必须` 为空。 485 | 486 | ```html 487 | HTTP/1.1 202 Accepted 488 | Server: nginx/1.11.9 489 | Content-Type: text/html; charset=UTF-8 490 | Transfer-Encoding: chunked 491 | Date: Sun, 24 Jun 2018 09:25:15 GMT 492 | Connection: keep-alive 493 | ``` 494 | 495 | ### 204 No Content 496 | 497 | 该状态码表示响应实体不包含任何数据,其中: 498 | 499 | * 在使用 `DELETE` 方法删除资源 **成功** 时,`必须` 返回该状态码 500 | * 使用 `PUT`、`PATCH` 方法更新数据 **成功** 时,也 `应该` 返回此状态码 501 | 502 | ```http 503 | HTTP/1.1 204 No Content 504 | Server: nginx/1.11.9 505 | Date: Sun, 24 Jun 2018 09:29:12 GMT 506 | Connection: keep-alive 507 | ``` 508 | 509 | ### 3xx 重定向 510 | 511 | 所有 `API` `不该` 返回 `3xx` 类型的状态码。因为 `3xx` 类型的响应格式一般为下列格式: 512 | 513 | ```html 514 | HTTP/1.1 302 Found 515 | Server: nginx/1.11.9 516 | Content-Type: text/html; charset=UTF-8 517 | Transfer-Encoding: chunked 518 | Cache-Control: no-cache, private 519 | Date: Sun, 24 Jun 2018 09:41:50 GMT 520 | Location: https://example.com 521 | Connection: keep-alive 522 | 523 | 524 | 525 |
526 | 527 | 528 | 529 |