本文是为 大渝网 (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 中嵌入版本编号
api.example.com/v1/*
这种做法是版本号直观、易于调试;另一种做法是,将版本号放在
HTTP
Header
头中:
通过媒体类型来指定版本信息
Accept: application/vnd.example.com.v1+json
其中
vnd
表示
Standards
Tree
标准树类型,有三个不同的树:
x
,
prs
和
vnd
。你使用的标准树需要取决于你开发的项目。
-
未注册的树(
x
)主要表示本地和私有环境
-
私有树(
prs
)主要表示没有商业发布的项目
-
供应商树(
vnd
)主要表示公开发布的项目
后面几个参数依次为应用名称(一般为应用域名)、版本号、期望的返回格式。
至于具体把版本号放在什么地方,这个问题一直存在很大的争议,但由于我们大多数时间都在使用
Laravel
开发,
应该
使用 dingo/api 来快速构建应用,它采用第二种方式来管理
API
版本,并且已集成了标准的
HTTP
Response
。
Endpoints
端点就是指向特定资源或资源集合的
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):从服务器删除资源。
其中:
-
删除资源
必须
用
DELETE
方法。
-
创建新的资源
必须
使用
POST
方法。
-
更新资源
应该
使用
PUT
方法。
-
获取资源信息
必须
使用
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
参数
必须
是全小写,
必须
使用下划线类型的参数形式。
经常使用的、复杂的查询
应该
标签化,降低维护成本。如
GET /trades?status=closed&sort=sortby=name&order=asc
# 可为其定制快捷方式
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
会在多少
秒
后失效。
{
"access_token": "token....",
"token_type": "Bearer",
"expires_in": 3600
}
客户端在请求需要认证的
API
时,
必须
在请求头
Authorization
中带上
access_token
。
Authorization: Bearer token...
当超过指定的秒数后,
access token
就会过期,再次用过期/或无效的
token
访问时,服务端
应该
返回
invalid_token
的错误或
401
错误码。
HTTP/1.1 401
Unauthorized
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
{
"error": "invalid_token"
}
Laravel 开发中,
应该
使用 JWT 来为管理你的 Token,并且
一定不可
在
api
中间件中开启请求
session
。
Response
所有的
API
响应,
必须
遵守
HTTP
设计规范,
必须
选择合适的
HTTP
状态码。
一定不可
所有接口都返回状态码为
200
的
HTTP
响应,如:
HTTP/1.1 200 ok
Content-
Type: application/json
Server: example.com
{
"code": 0,
"msg": "success",
"data": {
"username": "username"
}
}
或
HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com
{
"code":
-1,
"msg": "该活动不存在",
}
下表列举了常见的
HTTP
状态码:
状态码
|
描述
|
1xx
|
代表请求已被接受,需要继续处理
|
2xx
|
请求已成功,请求所希望的响应头或数据体将随此响应返回
|
3xx
|
重定向
|
4xx
|
客户端原因引起的错误
|
5xx
|
服务端原因引起的错误
|
只有来自客户端的请求被正确的处理后才能返回
2xx
的响应,所以当 API 返回
2xx
类型的状态码时,前端
必须
认定该请求已处理成功。
必须强调的是,所有
API
一定不可
返回
1xx
类型的状态码。当
API
发生错误时,
必须
返回出错时的详细信息。目前常见返回错误信息的方法有两种:
1、将错误详细放入
HTTP
响应首部:
X-MYNAME-ERROR-CODE: 4001
X-MYNAME-ERROR-MESSAGE: Bad authentication token
X-MYNAME-ERROR-INFO: http://docs.example.com/api/v1/authentication
2、直接放入响应实体中:
HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:02:59 GMT
Connection: keep-alive
{"error_code":40100,"message":"Unauthorized"}
考虑到易读性和客户端的易处理性,我们
必须
把错误信息直接放到响应实体中,并且错误格式
应该
满足如下格式:
{
"message": "您查找的资源不存在",
"error_code": 404001
}
其中错误码(
error_code
)
必须
和
HTTP
状态码对应,也方便错误码归类,如:
HTTP/1.1 429 Too Many Requests
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:
15:52 GMT
Connection: keep-alive
{"error_code":429001,"message":"你操作太频繁了"}
HTTP/1.1 403 Forbidden
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:19:27 GMT
Connection: keep-alive
{"error_code":403002,"message":"用户已禁用"}
应该
在返回的错误信息中,同时包含面向开发者和面向用户的提示信息,前者可方便开发人员调试,后者可直接展示给终端用户查看如:
{
"message": "直接展示给终端用户的错误信息",
"error_code": "业务错误码",
"error": "供开发者查看的错误信息",
"debug": [
"错误堆栈,必须开启 debug 才存在"
]
}
下面详细列举了各种情况 API 的返回说明。
200 ok
200
状态码是最常见的
HTTP
状态码,在所有
成功
的
GET
请求中,
必须
返回此状态码。
HTTP
响应实体部分
必须
直接就是数据,不要做多余的包装。
错误示例:
HTTP/1.1
200 ok
Content-Type: application/json
Server: example.com
{
"user": {
"id":1,
"nickname":"fwest",
"username": "example"
}
}
正确示例:
1、获取单个资源详情
{
"id": 1,
"username": "godruoyi",
"age": 88,
}
2、获取资源集合
[
{
"id": 1,
"username": "godruoyi",
"age": 88,
},
{
"id": 2,
"username": "foo",
"age": 88,
}
]
3、额外的媒体信息
{
"data": [
{
"id": 1,
"avatar": "https://lorempixel.com/640/480/?32556",
"nickname": "fwest",
"last_logined_time": "2018-05-29 04:56:43",
"has_registed": true
},
{
"id": 2,
"avatar": "https://lorempixel.com/640/480/?86144",
"nickname": "zschowalter",
"last_logined_time": "2018-06-16 15:18:34",
"has_registed": true
}
],
"meta": {
"pagination": {
"total": 101,
"count": 2,
"per_page": 2,
"current_page": 1,
"total_pages": 51,
"links": {
"next": "http://api.example.com?page=2"
}
}
}
}
其中,分页和其他额外的媒体信息,必须放到
meta
字段中。
201 Created
当服务器创建数据成功时,
应该
返回此状态码。常见的应用场景是使用
POST
提交用户信息,如:
等,都可以返回
201
状态码。需要注意的是,你可以选择在用户创建成功后返回新用户的数据:
HTTP/1.1 201 Created
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:13:40 GMT
Connection: keep-alive
{
"id": 1,
"avatar": "https:\/\/lorempixel.com\/640\/480\/?32556",
"nickname": "fwest",
"last_logined_time": "2018-05-29 04:56:43",
"created_at": "2018-06-16 17:55:55",
"updated_at": "2018-06-16 17:55:55"
}
也可以返回一个响应实体为空的
HTTP
Response
如:
HTTP/1.1 201 Created
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:12
:20 GMT
Connection: keep-alive
这里我们
应该
采用第二种方式,因为大多数情况下,客户端只需要知道该请求操作成功与否,并不需要返回新资源的信息。
202 Accepted
该状态码表示服务器已经接受到了来自客户端的请求,但还未开始处理。常用短信发送、邮件通知、模板消息推送等这类很耗时需要队列支持的场景中。
返回该状态码时,响应实体
必须
为空。
HTTP/1.1 202 Accepted
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:25:15 GMT
Connection: keep-alive
204 No Content
该状态码表示响应实体不包含任何数据,其中:
HTTP/1.1 204 No Content
Server: nginx/1.11.9
Date:
Sun, 24 Jun 2018 09:29:12 GMT
Connection: keep-alive
3xx 重定向
所有
API
一定不可
返回
3xx
类型的状态码。因为
3xx
类型的响应格式一般为下列格式:
HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 09:41:50 GMT
Location: https://example.com
Connection: keep-alive
charset="UTF-8" />
http-equiv
="refresh" content="0;url=https://example.com" />
Redirecting to https://example.com
Redirecting to href="https://example.com">https://example.com.
API
一定不可
返回纯
HTML
结构的响应;若一定要使用重定向功能,
应该
返回一个响应实体为空的
3xx
响应,并在响应头中加上
Location
字段:
HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:52:50 GMT
Location: https://godruoyi.com
Connection: keep-alive
400 Bad Request
由于明显的客户端错误(例如,请求语法格式错误、无效的请求、无效的签名等),服务器
应该
放弃该请求。
当服务器无法从其他 4xx 类型的状态码中找出合适的来表示错误类型时,都
必须
返回该状态码。
HTTP/1.1 400 Bad Request
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache
, private
Date: Sun, 24 Jun 2018 13:22:36 GMT
Connection: keep-alive
{"error_code":40000,"message":"无效的签名"}
401 Unauthorized
该状态码表示当前请求需要身份认证,以下情况都
必须
返回该状态码。
-
未认证用户访问需要认证的 API
-
access_token 无效/过期
客户端在收到
401
响应后,都
应该
提示用户进行下一步的登录操作。
HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
WWW-Authenticate: JWTAuth
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:17:02 GMT
Connection: keep-alive
"message":"Token Signature could not be verified.","error_code": "40100"}
403 Forbidden
该状态码可以简单的理解为没有权限访问该请求,服务器收到请求但拒绝提供服务。
如当普通用户请求操作管理员用户时,
必须
返回该状态码。
HTTP/1.1 403 Forbidden
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: