专栏名称: SegmentFault思否
SegmentFault (www.sf.gg)开发者社区,是中国年轻开发者喜爱的极客社区,我们为开发者提供最纯粹的技术交流和分享平台。
目录
相关文章推荐
OSC开源社区  ·  Bun ... ·  昨天  
程序员的那些事  ·  印度把 DeepSeek ... ·  3 天前  
OSC开源社区  ·  升级到Svelte ... ·  5 天前  
程序猿  ·  “我真的受够了Ubuntu!” ·  3 天前  
程序员的那些事  ·  成人玩偶 + ... ·  5 天前  
51好读  ›  专栏  ›  SegmentFault思否

RESTful API 设计规范

SegmentFault思否  · 公众号  · 程序员  · 2018-07-09 08:00

正文

本文是为 大渝网 (http://cq.qq.com) API 开发规范拟定的一个 beta 版,文章大量参考了目前比较常见的 RESTful API 设计。

为了更好的讨论规范带来的争议及问题,现已把该文档整理并开源到 github (https://github.com/godruoyi/restful-api-specification),关于大家补充及提 issue。

关于「能愿动词」的使用

为了避免歧义,文档大量使用了“能愿动词”,对应的解释如下:

  • 必须 ( MUST ) :绝对,严格遵循,请照做,无条件遵守;

  • 一定不可 ( MUST NOT ) :禁令,严令禁止;

  • 应该 ( SHOULD ) :强烈建议这样做,但是不强求;

  • 不该 ( SHOULD NOT ) :强烈不建议这样做,但是不强求;

  • 可以 ( MAY ) 可选 ( OPTIONAL ) :选择性高一点,在这个文档内,此词语使用较少;

参见:RFC 2119 (http://www.ietf.org/rfc/rfc2119.txt)

协议

在通过 API 于后端服务通信的过程中, 应该 使用 HTTPS 协议。

API Root URL

API 的根入口点应尽可能保持足够简单,这里有两个常见的 URL 根例子:

  • api.example.com/*

  • example.com/api/*

如果你的应用很庞大或者你预计它将会变的很庞大,那 应该 API 放到子域下。这种做法可以保持某些规模化上的灵活性。

Versioning

所有的 API 必须保持向后兼容,你 必须 在引入新版本 API 的同时确保旧版本 API 仍然可用。所以 应该 为其提供版本支持。

目前比较常见的两种版本号形式:

在 URL 中嵌入版本编号
  1. api.example.com/v1/*

这种做法是版本号直观、易于调试;另一种做法是,将版本号放在 HTTP Header 头中:

通过媒体类型来指定版本信息
  1. Accept: application/vnd.example.com.v1+json

其中 vnd 表示 Standards Tree 标准树类型,有三个不同的树: x prs vnd 。你使用的标准树需要取决于你开发的项目。

  • 未注册的树( x )主要表示本地和私有环境

  • 私有树( prs )主要表示没有商业发布的项目

  • 供应商树( vnd )主要表示公开发布的项目

后面几个参数依次为应用名称(一般为应用域名)、版本号、期望的返回格式。

至于具体把版本号放在什么地方,这个问题一直存在很大的争议,但由于我们大多数时间都在使用 Laravel 开发, 应该 使用 dingo/api 来快速构建应用,它采用第二种方式来管理 API 版本,并且已集成了标准的 HTTP Response

Endpoints

端点就是指向特定资源或资源集合的 URL 。在端点的设计中,你 必须 遵守下列约定:

  • URL 的命名 必须 全部小写

  • URL 中资源( resource )的命名 必须 是名词,并且 必须 是复数形式

  • 必须 优先使用 Restful 类型的 URL

  • URL 中不能出现 - 必须 用下划线 _ 代替

  • URL 必须 是易读的

  • URL 一定不可 暴露服务器架构

来看一个反例:

  • https://api.example.com/getUserInfo?userid=1

  • https://api.example.com/getusers

  • https://api.example.com/sv/u

  • https://api.example.com/cgi-bin/users/get_user.php?userid=1

再来看一个正例:

  • https://api.example.com/zoos

  • https://api.example.com/animals

  • https://api.example.com/zoos/{zoo}/animals

  • https://api.example.com/animal_types

  • https://api.example.com/employees

HTTP 动词

对于资源的具体操作类型,由 HTTP 动词表示。常用的 HTTP 动词有下面五个(括号里是对应的 SQL 命令)。

  • GET(SELECT):从服务器取出资源(一项或多项)。

  • POST(CREATE):在服务器新建一个资源。

  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。

  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。

  • DELETE(DELETE):从服务器删除资源。

其中:

  1. 删除资源 必须 DELETE 方法。

  2. 创建新的资源 必须 使用 POST 方法。

  3. 更新资源 应该 使用 PUT 方法。

  4. 获取资源信息 必须 使用 GET 方法。

针对每一个端点来说,下面列出所有可行的 HTTP 动词和端点的组合:

请求方法 URL 描述
GET /zoos 列出所有的动物园(ID和名称,不要太详细)
POST /zoos 新增一个新的动物园
GET /zoos/{zoo} 获取指定动物园详情
PUT /zoos/{zoo} 更新指定动物园(整个对象)
PATCH /zoos/{zoo} 更新动物园(部分对象)
DELETE /zoos/{zoo} 删除指定动物园
GET /zoos/{zoo}/animals 检索指定动物园下的动物列表(ID和名称,不要太详细)
GET /animals 列出所有动物(ID和名称)。
POST /animals 新增新的动物
GET /animals/{animal} 获取指定的动物详情
PUT /animals/{animal} 更新指定的动物(整个对象)
PATCH /animals/{animal} 更新指定的动物(部分对象)
GET /animal_types 获取所有动物类型(ID和名称,不要太详细)
GET /animal_types/{type} 获取指定的动物类型详情
GET /employees 检索整个雇员列表
GET /employees/{employee} 检索指定特定的员工
GET /zoos/{zoo}/employees 检索在这个动物园工作的雇员的名单(身份证和姓名)
POST /employees 新增指定新员工
POST /zoos/{zoo}/employees 在特定的动物园雇佣一名员工
DELETE /zoos/{zoo}/employees/{employee} 从某个动物园解雇一名员工

Filtering

如果记录数量很多,服务器不可能都将它们返回给用户。API 应该 提供参数,过滤返回结果。下面是一些常见的参数。

  • ?limit=10:指定返回记录的数量。

  • ?offset=10:指定返回记录的开始位置。

  • ?page=2&per_page=100:指定第几页,以及每页的记录数。

  • ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。

  • ?animal type id=1:指定筛选条件。

所有 URL 参数 必须 是全小写, 必须 使用下划线类型的参数形式。

经常使用的、复杂的查询 应该 标签化,降低维护成本。如

  1. GET /trades?status=closed&sort=sortby=name&order=asc

  2. # 可为其定制快捷方式

  3. GET /trades/recently_closed

Authentication

应该 使用 OAuth2 . 0 的方式为 API 调用者提供登录认证。 必须 先通过登录接口获取 Access Token 后再通过该 token 调用需要身份认证的 API

Oauth 的端点设计示列:

  • RFC 6749 /token

  • Twitter /oauth2/token

  • Fackbook /oauth/access_token

  • Google /o/oauth2/token

  • Github /login/oauth/access_token

  • Instagram /oauth/authorize

客户端在获得 access token 的同时 必须 在响应中包含一个名为 expires_in 的数据,它表示当前获得的 token 会在多少 后失效。

  1. {

  2.    "access_token": "token....",

  3.    "token_type": "Bearer",

  4.    "expires_in": 3600

  5. }

客户端在请求需要认证的 API 时, 必须 在请求头 Authorization 中带上 access_token

  1. Authorization: Bearer token...

当超过指定的秒数后, access token 就会过期,再次用过期/或无效的 token 访问时,服务端 应该 返回 invalid_token 的错误或 401 错误码。

  1. HTTP/1.1 401 Unauthorized

  2. Content-Type: application/json

  3. Cache-Control: no-store

  4. Pragma: no-cache

  5. {

  6.    "error": "invalid_token"

  7. }

Laravel 开发中, 应该 使用 JWT 来为管理你的 Token,并且 一定不可 api 中间件中开启请求 session

Response

所有的 API 响应, 必须 遵守 HTTP 设计规范, 必须 选择合适的 HTTP 状态码。 一定不可 所有接口都返回状态码为 200 HTTP 响应,如:

  1. HTTP/1.1 200 ok

  2. Content- Type: application/json

  3. Server: example.com

  4. {

  5.    "code": 0,

  6.    "msg": "success",

  7.    "data": {

  8.        "username": "username"

  9.    }

  10. }

  1. HTTP/1.1 200 ok

  2. Content-Type: application/json

  3. Server: example.com

  4. {

  5.    "code": -1,

  6.    "msg": "该活动不存在",

  7. }

下表列举了常见的 HTTP 状态码:

状态码 描述
1xx 代表请求已被接受,需要继续处理
2xx 请求已成功,请求所希望的响应头或数据体将随此响应返回
3xx 重定向
4xx 客户端原因引起的错误
5xx 服务端原因引起的错误

只有来自客户端的请求被正确的处理后才能返回 2xx 的响应,所以当 API 返回 2xx 类型的状态码时,前端 必须 认定该请求已处理成功。

必须强调的是,所有 API 一定不可 返回 1xx 类型的状态码。当 API 发生错误时, 必须 返回出错时的详细信息。目前常见返回错误信息的方法有两种:

1、将错误详细放入 HTTP 响应首部:

  1. X-MYNAME-ERROR-CODE: 4001

  2. X-MYNAME-ERROR-MESSAGE: Bad authentication token

  3. X-MYNAME-ERROR-INFO: http://docs.example.com/api/v1/authentication

2、直接放入响应实体中:

  1. HTTP/1.1 401 Unauthorized

  2. Server: nginx/1.11.9

  3. Content-Type: application/json

  4. Transfer-Encoding: chunked

  5. Cache-Control: no-cache, private

  6. Date: Sun, 24 Jun 2018 10:02:59 GMT

  7. Connection: keep-alive

  8. {"error_code":40100,"message":"Unauthorized"}

考虑到易读性和客户端的易处理性,我们 必须 把错误信息直接放到响应实体中,并且错误格式 应该 满足如下格式:

  1. {

  2.    "message": "您查找的资源不存在",

  3.    "error_code": 404001

  4. }

其中错误码( error_code 必须 HTTP 状态码对应,也方便错误码归类,如:

  1. HTTP/1.1 429 Too Many Requests

  2. Server: nginx/1.11.9

  3. Content-Type: application/json

  4. Transfer-Encoding: chunked

  5. Cache-Control: no-cache, private

  6. Date: Sun, 24 Jun 2018 10: 15:52 GMT

  7. Connection: keep-alive

  8. {"error_code":429001,"message":"你操作太频繁了"}

  1. HTTP/1.1 403 Forbidden

  2. Server: nginx/1.11.9

  3. Content-Type: application/json

  4. Transfer-Encoding: chunked

  5. Cache-Control: no-cache, private

  6. Date: Sun, 24 Jun 2018 10:19:27 GMT

  7. Connection: keep-alive

  8. {"error_code":403002,"message":"用户已禁用"}

应该 在返回的错误信息中,同时包含面向开发者和面向用户的提示信息,前者可方便开发人员调试,后者可直接展示给终端用户查看如:

  1. {

  2.    "message": "直接展示给终端用户的错误信息",

  3.    "error_code": "业务错误码",

  4.    "error": "供开发者查看的错误信息",

  5.    "debug": [

  6.        "错误堆栈,必须开启 debug 才存在"

  7.    ]

  8. }

下面详细列举了各种情况 API 的返回说明。

200 ok

200 状态码是最常见的 HTTP 状态码,在所有 成功 GET 请求中, 必须 返回此状态码。 HTTP 响应实体部分 必须 直接就是数据,不要做多余的包装。

错误示例:

  1. HTTP/1.1 200 ok

  2. Content-Type: application/json

  3. Server: example.com

  4. {

  5.    "user": {

  6.        "id":1,

  7.        "nickname":"fwest",

  8.        "username": "example"

  9.    }

  10. }

正确示例:

1、获取单个资源详情

  1. {

  2.    "id": 1,

  3.    "username": "godruoyi",

  4.    "age": 88,

  5. }

2、获取资源集合

  1. [

  2.    {

  3.        "id": 1,

  4.        "username": "godruoyi",

  5.        "age": 88,

  6.    },

  7.    {

  8.        "id": 2,

  9.        "username": "foo",

  10.        "age": 88,

  11.    }

  12. ]

3、额外的媒体信息

  1. {

  2.    "data": [

  3.         {

  4.            "id": 1,

  5.            "avatar": "https://lorempixel.com/640/480/?32556",

  6.            "nickname": "fwest",

  7.            "last_logined_time": "2018-05-29 04:56:43",

  8.            "has_registed": true

  9.        },

  10.        {

  11.            "id": 2,

  12.            "avatar": "https://lorempixel.com/640/480/?86144",

  13.            "nickname": "zschowalter",

  14.            "last_logined_time": "2018-06-16 15:18:34",

  15.            "has_registed": true

  16.        }

  17.     ],

  18.    "meta": {

  19.        "pagination": {

  20.            "total": 101,

  21.            "count": 2,

  22.            "per_page": 2,

  23.            "current_page": 1,

  24.            "total_pages": 51,

  25.            "links": {

  26.                "next": "http://api.example.com?page=2"

  27.            }

  28.        }

  29.    }

  30. }

其中,分页和其他额外的媒体信息,必须放到 meta 字段中。

201 Created

当服务器创建数据成功时, 应该 返回此状态码。常见的应用场景是使用 POST 提交用户信息,如:

  • 添加了新用户

  • 上传了图片

  • 创建了新活动

等,都可以返回 201 状态码。需要注意的是,你可以选择在用户创建成功后返回新用户的数据:

  1. HTTP/1.1 201 Created

  2. Server: nginx/1.11.9

  3. Content-Type: application/json

  4. Transfer-Encoding: chunked

  5. Date: Sun, 24 Jun 2018 09:13:40 GMT

  6. Connection: keep-alive

  7. {

  8.    "id": 1,

  9.    "avatar": "https:\/\/lorempixel.com\/640\/480\/?32556",

  10.    "nickname": "fwest",

  11.    "last_logined_time": "2018-05-29 04:56:43",

  12.    "created_at": "2018-06-16 17:55:55",

  13.    "updated_at": "2018-06-16 17:55:55"

  14. }

也可以返回一个响应实体为空的 HTTP Response 如:

  1. HTTP/1.1 201 Created

  2. Server: nginx/1.11.9

  3. Content-Type: text/html; charset=UTF-8

  4. Transfer-Encoding: chunked

  5. Date: Sun, 24 Jun 2018 09:12 :20 GMT

  6. Connection: keep-alive

这里我们 应该 采用第二种方式,因为大多数情况下,客户端只需要知道该请求操作成功与否,并不需要返回新资源的信息。

202 Accepted

该状态码表示服务器已经接受到了来自客户端的请求,但还未开始处理。常用短信发送、邮件通知、模板消息推送等这类很耗时需要队列支持的场景中。

返回该状态码时,响应实体 必须 为空。

  1. HTTP/1.1 202 Accepted

  2. Server: nginx/1.11.9

  3. Content-Type: text/html; charset=UTF-8

  4. Transfer-Encoding: chunked

  5. Date: Sun, 24 Jun 2018 09:25:15 GMT

  6. Connection: keep-alive

204 No Content

该状态码表示响应实体不包含任何数据,其中:

  • 在使用 DELETE 方法删除资源 成功 时, 必须 返回该状态码

  • 使用 PUT PATCH 方法更新数据 成功 时,也 应该 返回此状态码

  1. HTTP/1.1 204 No Content

  2. Server: nginx/1.11.9

  3. Date: Sun, 24 Jun 2018 09:29:12 GMT

  4. Connection: keep-alive

3xx 重定向

所有 API 一定不可 返回 3xx 类型的状态码。因为 3xx 类型的响应格式一般为下列格式:

  1. HTTP/1.1 302 Found

  2. Server: nginx/1.11.9

  3. Content-Type: text/html; charset=UTF-8

  4. Transfer-Encoding: chunked

  5. Cache-Control: no-cache, private

  6. Date: Sun, 24 Jun 2018 09:41:50 GMT

  7. Location: https://example.com

  8. Connection: keep-alive

  9.    

  10.         charset="UTF-8" />

  11.         http-equiv ="refresh" content="0;url=https://example.com" />

  12.        Redirecting to https://example.com

  13.    

  14.    

  15.        Redirecting to href="https://example.com">https://example.com.

  16.    

API 一定不可 返回纯 HTML 结构的响应;若一定要使用重定向功能, 应该 返回一个响应实体为空的 3xx 响应,并在响应头中加上 Location 字段:

  1. HTTP/1.1 302 Found

  2. Server: nginx/1.11.9

  3. Content-Type: text/html; charset=UTF-8

  4. Transfer-Encoding: chunked

  5. Date: Sun, 24 Jun 2018 09:52:50 GMT

  6. Location: https://godruoyi.com

  7. Connection: keep-alive

400 Bad Request

由于明显的客户端错误(例如,请求语法格式错误、无效的请求、无效的签名等),服务器 应该 放弃该请求。

当服务器无法从其他 4xx 类型的状态码中找出合适的来表示错误类型时,都 必须 返回该状态码。

  1. HTTP/1.1 400 Bad Request

  2. Server: nginx/1.11.9

  3. Content-Type: application/json

  4. Transfer-Encoding: chunked

  5. Cache-Control: no-cache , private

  6. Date: Sun, 24 Jun 2018 13:22:36 GMT

  7. Connection: keep-alive

  8. {"error_code":40000,"message":"无效的签名"}

401 Unauthorized

该状态码表示当前请求需要身份认证,以下情况都 必须 返回该状态码。

  • 未认证用户访问需要认证的 API

  • access_token 无效/过期

客户端在收到 401 响应后,都 应该 提示用户进行下一步的登录操作。

  1. HTTP/1.1 401 Unauthorized

  2. Server: nginx/1.11.9

  3. Content-Type: application/json

  4. Transfer-Encoding: chunked

  5. WWW-Authenticate: JWTAuth

  6. Cache-Control: no-cache, private

  7. Date: Sun, 24 Jun 2018 13:17:02 GMT

  8. Connection: keep-alive

  9. "message":"Token Signature could not be verified.","error_code": "40100"}

403 Forbidden

该状态码可以简单的理解为没有权限访问该请求,服务器收到请求但拒绝提供服务。

如当普通用户请求操作管理员用户时, 必须 返回该状态码。

  1. HTTP/1.1 403 Forbidden

  2. Server: nginx/1.11.9

  3. Content-Type: application/json

  4. Transfer-Encoding:







请到「今天看啥」查看全文