Restful API 设计
Oct 25, 2016
REST(Representational State Transfer) 是 Roy Fielding 博士在2000年他的博士论文「Architectural Styles and the Design of Network-based Software Architectures(PDF)」中提出来的一种软件架构风格。REST 服务与早前 Web Service 的 SOAP 和 XML-RPC 协议对比来讲更加简洁,现在越来越多的 Web 服务开始采用 REST 风格设计和实现。
Restful API 设计就是设计具有 REST 风格的 Web API。
一、名词解释
-
资源(resource):一个操作对象的抽象实例。例如:公交系统中的某张票卡(ticket),管理系统中的某个用户(user)等。
-
集合(collection):一种资源。使用资源的复数表示。例如:公交系统中的所有票卡(tickets),管理系统中的所有用户(users)等。
-
调用方(consumer):调用 API 的一方。HTTP 请求由调用方发起。也翻译成“消费者”、“客户端”、“应用”。
-
服务端(server):提供 API 服务的一方。HTTP 响应由服务端返回。也翻译成“服务”、“服务方”、“被调用方”。
-
接口(endpoint):一个 API。表现形式是 URL 地址,指向一个资源或一种资源。也翻译成“终点”、“端点”。英文中也有“entry point”的用法。
-
幂等性(idempotent):HTTP 的幂等性。指 API 无论调用一次还是多次,所产生的结果是一样的。
二、关于 URL 的设计
2.1. 使用 API 域
建议使用api
作为一级目录。好处是服务端可以方便扩展 API 以外的服务。例如,
https://example.com/api/... <-- 子域为API,返回数据是 json 形式
https://example.com/doc/... <-- 子域为文档,返回数据是 html 形式
另外一种设计是把api
作为二级域名。例如,
https://api.example.com <-- API地址,以下的 URL 全部是 API
https://doc.example.com <-- 文档地址,以下是 html 格式的文档
当网站很大时,api
也可以作为模块下的一级目录。例如,
https://example.com/shop/api/... <-- 商店子系统的API
https://example.com/shop/doc/... <-- 商店子系统的文档
https://example.com/shop/web/... <-- 商店子系统的页面
https://example.com/store/api/... <-- 仓库子系统的API
https://example.com/store/wiki/... <-- 仓库子系统的wiki页面
注:以api
命名不是必须的,但是使用api
来标识 API 的域会更容易理解。
2.2. 使用版本
建议在 URL 中包含 API 的版本号。好处是:1. API 升级不影响调用方的代码,2. 升级后的 API 可以不向前兼容。
https://example.com/api/v1/... <-- 第一版 API
https://example.com/api/v2/... <-- 第二版 API
另外,还有其他两种设计。
2.2.1. 使用 accept header 标识版本
调用方在 HTTP 请求的 header 中写明所需的 API 版本,服务端识别后,使用相应版本的 API 处理调用方的请求。
Accept: application/json+v1
2.2.2. 使用自定义的 HTTP header 标识版本
调用方在 HTTP 请求的 header 中加入自定义的 header 项来告诉服务端所调用的 API 版本。
X-Api-Version: 1
2.3. 资源的命名
2.3.1. 使用名词
/get <-- bad
/getTickets <-- bad
/tickets <-- good
2.3.2. 使用中横线或下划线
资源的名称中可以包含中横线(-)或下划线(_),在系统设计时应遵循统一的规则,即统一使用中横线(kebab-case rule)或统一使用下划线(snake_case rule)。
2.3.3. 使用小写
建议使用小写字母命名资源。在需要多个名词组合时,一般用中横线或下划线分隔,而不使用驼峰命名法(camelCase rule)。
/lockedAccount <-- bad
/locked-account <-- good
/locked_account <-- good
2.4. 使用集合表示资源种类,资源的 ID 标识的资源实例
/ticket <-- bad
/tickets <-- good,表示系统中的所有票卡
/tickets/1 <-- good,表示编号为1的票卡
2.5. 使用层级结构的 URL 描述资源间关系
层级结构范式:/:col/:id/:col/:id/:col/...
/departments <-- 公司中的所有部门
/departments/3 <-- 编号为3的部门
/departments/3/employees <-- 编号为3的部门下所有员工
/departments/3/employees/1105 <-- 编号为3的部门中第1105号员工
2.6. 使用 URL 参数约束返回结果集
一般的,使用 URL 的参数来标识对返回结果集的约束,且这些约束可以叠加。
下面是一些常用的约束,和这些约束的表达方式。
2.6.1. 过滤
GET /cars?color=red <-- 获取所有红颜色的车
2.6.2. 排序
GET /cars?sort=-manufactorer,+model <-- 按生产商倒序,型号升序排序
更简单的排序设计。
GET /cars?sortby=model&order=asc <-- 按型号升序排序
2.6.3. 分页
GET /cars?offset=10&limit=5 <-- 取第10到第15辆车
_注:分页的全体数量可以使用自定义的 HTTP header 项返回,例如,
GET /cars?offset=10&limit=5 HTTP/1.1
...
X-Total-Count: 23
2.6.4. 限定
GET /cars?fields=manufacturer,model,id,color <-- 只取出车的生产商、型号、编号和颜色四个字段
2.6.5. 搜索
GET /cars?name=la* <-- 搜索车名以“la”开头的车
三、关于协议的设计
3.1. 使用 HTTP 动词(Verbs)
调用方使用 HTTP 动词来描述对资源的操作。其中,基本操作分别是,
- GET:获取资源;
- POST:创建资源;
- PUT:更新资源;
- DELETE:删除资源。
另外还有两个比较常用,
- PATCH:更新资源的一部分;
- HEAD:获取资源的描述数据(meta data)。
注:OPTION
在实际中用到较少。
3.2. 返回 HTTP 状态码(HTTP Status Code)
服务端通过返回不同的 HTTP 状态码(HTTP Status Codes1)来标识执行的结果。
3.2.1. 主要状态码
- 2xx :成功
- 4xx :调用方错误
- 5xx :服务端错误
3.2.2. 调用成功的状态码
- 200:GET 操作成功返回状态码 200,代表正常返回数据;
- 201:POST/PUT/PATCH 操作成功返回状态码 201,代表成功创建/更新资源;
- 204:DELETE 操作成功返回状态码 204,代表成功删除资源;
3.2.3. 调用失败的状态码
- 400:请求无效。通常是指请求的参数错误,具体错误描述应包含在 response body 中;
- 401:请求的资源需要授权。服务端首先要验证调用方的身份才能继续处理请求;
- 403:拒绝服务。通常是指服务端已经知道调用方的身份(如果需要授权),但拒绝服务2;
- 404:请求不存在的资源;
- 405:资源无法执行请求的操作。例如,对只读资源执行 DELETE 操作;
- 429:请求过于频繁,或服务端忙,暂时无法处理请求;
- 500:服务端错误。
3.3. 数据载体的设计
3.3.1. 只使用JSON
使用JSON作为数据的载体是 RESTful API 的趋势3。应避免使用 XML 作为数据载体。4
3.3.2. 统一数据载体的形式
统一请求数据和响应数据的载体,避免请求使用 form 表单,响应使用JSON。5
3.3.3. 在 HTTP 头部明示数据载体
请求数据的头部明示数据载体,
Accept: application/json
Content-Type: application/json
响应数据的头部明示数据载体,
Content-Type: application/json
注:为了方便调试,有些 best practices也建议在 URL 的尾部加上 .json
4。
3.4. 数据内容的设计
3.4.1. 使用 Google 风格
建议采用现在比较流行的 Google 风格6。
在 Google 风格中,正常返回{ok:...}
,错误返回{error:...}
。一个 response 可以有一个 ok
对象或者一个 error
对象,但不能两者都包含。如果 ok
和 error
都出现,则 error
对象优先。
这样做,调用方就可以用 resp.ok
和 resp.error
轻松判断是正常返回了数据还是发生了错误。
if (resp.ok) {
// 获取返回数据
} else if (resp.error) {
// 提取错误消息
} else {
// 系统级错误
}
正常返回的数据包含在 resp.ok
中,可以是对象({...}
)或者是数组([...]
)。
错误数据的格式一般设计为,
{
"error": {
"code": 404, <-- 全局错误编号
"message": "File Not Found", <-- 全局错误消息
"errors": [{ <-- 错误集合
"code": 2560, <-- 集合中每个错误编号
"message": "File Not Found", <-- 集合中每个错误的消息
"error": "..." <-- 错误对象实例
}]
}
}
注:在 Google 风格中,响应数据的正误属性是 data
和 error
。本文改为 ok
和error
。
3.4.2. 使用ISO-86017
RESTful API 没有标准的日期和时间格式,约定俗成,应使用 ISO-8601 作为日期和时间格式。8
3.4.2. 不要使用HATEOAS9
使用 HATEOAS10 会把部署时的问题引入开发阶段,建议避免使用HATEOAS。
3.4.3. 忽略null
属性值为 null 的属性应该被忽略(不会出现在返回值中)。
3.5. 高级应用
3.5.1. X-HTTP-Method-Override
由于调用方可能需要通过代理服务器来访问 RESTful API,而代理服务器可能不完全支持所有的 HTTP 动词(例如,通过只支持 GET 和 POST 的代理服务器),为了提高RESTful API 的可用性,一般采用在 HTTP 头部添加自定义 header 项的办法来覆盖调用方的 HTTP 动词。
例如,约定如果存在自定义 header 项 X-HTTP-Method-Override
,则使用自定义 header 项 X-HTTP-Method-Override
设置的动词覆盖 HTTP 动词。
3.5.2. 速率限制
为了确保 RESTful API 的稳定性,防止恶意攻击,需要限制单位时间内 RESTful API 的调用次数。一般的,使用令牌桶算法(token bucket algorithm11)来限制请求速率。
使用令牌桶算法限制请求速率,一般的,需要在响应的头部包含以下自定义 header 项,
X-Rate-Limit-Limit
:当期允许请求的次数X-Rate-Limit-Remaining
:当期剩余的请求次数X-Rate-Limit-Reset
:当期剩余的秒数
当在单位时间内超出了规定的请求次数时,通常返回 HTTP 状态码 429。
3.5.3. 缓存
RESTful API 采用和 HTTP 协议相同的缓存机制12。有两种做法:1. ETag + If-None-Match13;2. Last-Modified + If-Modified-Since14。这两种做法的效果相同。
3.5.4. 支持 CORS15 和 JSONP16
由于调用方可能是不同域(domain)的 Javascript 应用,服务端应实现 CORS。
同时,由于有一些场合(例如,使用老版本的浏览器)可能不支持CORS,所以作为一个健壮的服务端,应该支持 JSONP 调用。
GET /orders <-- CORS 调用
GET /orders.jsonp?callback=foo <-- JSONP调用
四、关于安全的设计
4.1. 总是使用HTTPS协议17
4.2. 认证
由于 RESTful API 是无状态的,所以不能通过 cookie 或 session 这样的机制认证。
4.2.1. 使用 HTTP 基本认证18
因为 RESTful API 使用 HTTPS 协议,所以可以使用基于 token 的 HTTP 基本认证。
4.2.2. 使用 OAuth 2.019
有时为了避免调用方获取 token 或调用方可能无法获得 token,需要使用 OAuth 认证。