Mongoose House Technical Edition

Restful API 设计

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。

一、名词解释

  1. 资源(resource):一个操作对象的抽象实例。例如:公交系统中的某张票卡(ticket),管理系统中的某个用户(user)等。

  2. 集合(collection):一种资源。使用资源的复数表示。例如:公交系统中的所有票卡(tickets),管理系统中的所有用户(users)等。

  3. 调用方(consumer):调用 API 的一方。HTTP 请求由调用方发起。也翻译成“消费者”、“客户端”、“应用”。

  4. 服务端(server):提供 API 服务的一方。HTTP 响应由服务端返回。也翻译成“服务”、“服务方”、“被调用方”。

  5. 接口(endpoint):一个 API。表现形式是 URL 地址,指向一个资源或一种资源。也翻译成“终点”、“端点”。英文中也有“entry point”的用法。

  6. 幂等性(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 的尾部加上 .json4

3.4. 数据内容的设计

3.4.1. 使用 Google 风格

建议采用现在比较流行的 Google 风格6

在 Google 风格中,正常返回{ok:...},错误返回{error:...}。一个 response 可以有一个 ok 对象或者一个 error 对象,但不能两者都包含。如果 okerror 都出现,则 error 对象优先。

这样做,调用方就可以用 resp.okresp.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 风格中,响应数据的正误属性是 dataerror。本文改为 okerror

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 认证。

五、参考

  1. Principles of good RESTful API Design

  2. 10 Best Practices for Better RESTful API

  3. HTTP API Design Guide

  4. Best Practices for Designing a Pragmatic RESTful API

  5. REST best practices

  6. Restful API 的设计规范

  7. How to design a REST API

六、引用