RESTful接口设计指南

RESTful是Representational State Transfer的缩写,直译为表现层状态转换。它不是一种标准或要求,而是一种设计风格和原则。如果一个API或架构符合REST的原则,我们就可以称之为RESTful API或RESTful Architecture。作为一种流行、成熟的设计原则,RESTful接口是目前不同端 (e.g. Mobile-Backend, Web-Backend)之间广泛利用的交互格式。

基本概念

要理解Representational State Transfer的含义,我们需要把REST拆开,首先要明白什么是资源(Resource),其次是表现层(Representation),最后是状态转换(State Transfer)。厘清了基本概念,才能更好地设计接口。

Representation of Resource

表现层实际指的是Resource的表现层,因此严格来说REST的全称应为The Representational State Transfer of Resource。Resource指的是一个具体的实例对象,比如一段文本,一个JSON对象,一张图片等,某个URI指向这个对象。而表现层就是Resource的表现形式,比如文本的JSON或XML格式。因此在具体的处理上,URI应该仅仅表示资源位置,而其表现形式应该在HTTP的请求头里指定。

1
2
http://example.com/some_text
Content-Type text/plain

Transfer of State

当客户端与服务器通信时,需要操作Resource使之状态发生变化,即State Transfer。而这种transfer需要通过表现层来实现,通常借助于HTTP协议。参与此过程的各个角色的作用如下:
URI:定位Resource
HTTP动词:Resource将如何进行State Transfer
HTTP请求头:Resource的表现形式

设计原则

版本

接口的开发免不了迭代,因此版本的划分是第一件要考虑的事情。版本的迭代应该尽量做到平滑(smoothed),更新旧接口时不应直接把它干掉,而应该以版本编号来区分。
以下两种方案是处理API版本的常见策略,各有优劣。

  • 由URL区分
    这个方案直接将版本指定在URL里,比较直观,但是会增加URL的复杂度。
    1
    /api/v1/...

示例
Request

1
GET /api/v1/...

Response

1
HTTP 1.1 200 OK

  • 置于请求/响应头
    这个方案将版本信息放在header里,会增加交互双方的处理成本,但是能保持URL的一致。
    1
    2
    3
    /api/...
    请求头包含:
    Accept application/myapp[.version]

示例
Request

1
2
3
GET /api/...
请求头包含:
Accept application/myapp.v2

Response

1
2
3
HTTP 1.1 200 OK
响应头包含:
X-MyApp-Media-Type: myapp.v2

根URL

根URL相当于功夫的起手势,是所有API的根路径,与版本一样通常也有两套方案。

  • 根域名下的路径

    1
    https://myapp.com/api/
  • 子域名

    1
    https://api.myapp.com/

此外,访问根URL的时候应该返回Endpoint的列表(Endpoint在下文描述),这样当Developer需要使用你的API时,在文档未完善前,可以访问根URL获取API的基本信息;文档完善时,也可以返回给Developer获取API文档的办法,同时返回的信息本身可以作为一个快速、简要的文档。

Endpoint

在没有RESTful的混沌时期,你可能见过这种“似乎”一目了然的URL:

1
GET http://example.com/myapp/getStudent?id=2

或者

1
2
POST http://example.com/myapp/deleteStudent (id=2)
POST http://example.com/myapp/saveStudent (id=2, name=rob)

然而,这种动宾搭配太啰嗦了,而且完全没有利用HTTP的动词(HTTP动词将在下文解释),估计API的使用者会吐槽得很厉害,其实这是缺乏对Endpoint进行设计的严肃问题。
Endpoint紧随根URL,代表一个Resource或Resource的集合,应当只使用名词,且与数据库表对应起来。此外,应该尽量使用复数形式,因为在英文中,单复数混合作宾语时宾语被认为是复数的。
所以对比起来,

1
GET /students

1
GET /getStudents

前者显然比后者简洁。

HTTP动词

根URL和Endpoint共同定位了Resource,而如何操作Resource, 由HTTP动词和参数(如有)共同决定。参数的格式通常比较简单(key-value, JSON, 或位于URL路径中),而HTTP动词包括:

1
GET [SELECT] 获取resource

1
POST [CREATE] 新建resource
1
PUT [UPDATE] 更新resource, 客户端提供完整的新资源
1
PATCH [UPDATE] 更新resource, 客户端只需要提供改变的属性
1
DELETE [DELETE] 删除resource
1
HEAD 获取resource的元数据(metadata)
1
OPTIONS 获取关于resource的哪些属性可以被客户端更改的信息

最常见的是GET, POST, PUT以及DELETE。
e.g.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
GET /classes 获取所有classes
POST /classes (name=x) 新建class
GET /classes/{id} 根据id获取class
PUT /classes/{id} (name=y, teacher=rob) 更新指定id的class
PATCH /classes/{id} (name=z) 更新指定id的class
DELETE /classes/{id} 根据id删除class
GET /classes/id/students 获取属于指定id的class的所有students

GET /students 获取所有students
POST /students 新建student
GET /students/{id} 根据id获取student
PUT /students/{id} (name=tom, gender=male) 更新指定id的student
PATCH /students/{id} (gender=female) 更新指定id的student
DELETE /students/id 根据id删除指定的student

GET /teachers 获取所有teacher
GET /teachers/{id} 根据id获取teacher

GET /classes/{id}/teachers 获取任教于指定id的class的所有teachers
POST /classes/{cid}/teachers (tid=2) 指派某个teacher(tid)到指定id(cid)的class任教
DELETE /classes/{cid}/teachers (tid=2) 解除某个teacher(tid)在指定id(cid)的class的任职

Filtering

有时我们需要对返回的数据进行过滤,最常见的是在GET请求里,根据请求URL的参数过滤返回结果。
e.g.

1
2
3
4
5
6
?limit=10
?offset=3
?page=3&rows=10
?sortBy=name&order=desc
?gender=female
?q=searchvalue

API路径和过滤参数的冗余是可以存在的,e.g.

1
GET /classes/2/students

1
GET /students?classId=2

Status Codes

HTTP响应信息的状态码同样是RESTful API设计的重要一环,对于成功的请求,不应该偷懒只用200状态码来表示;对于失败的请求,同样不应只用500来表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
200 OK - [GET] 新建成功
201 CREATED - [POST/PUT/PATCH] 更新成功
202 Accepted - [*] 异步任务, 请求已排队
204 NO CONTENT - [DELETE] 删除成功

400 INVALID REQUEST - [POST/PUT/PATCH] 请求有误
401 Unauthorized - [所有] 无权限(token, 用户名/密码错误等权限认证问题)
403 Forbidden - [所有] 已得到权限, 但访问禁止(权限不够)
404 NOT FOUND - [所有] resource不存在
406 Not Acceptable - [GET] 请求格式不可得, e.g. 请求JSON, 只有XML可用
410 Gone - [GET] resource被永久删除
500 INTERNAL SERVER ERROR - [所有] 服务器内部错误

100-199关于HTTP协议的底层信息,一般不会手动发送。
200-299是请求成功(操作成功或成功获取到所需的数据)的标志。
300-399是为流量重定向准备的,在超媒体相关的API中会使用到。
400-499表示错误信息,比如请求的参数有误,请求的数据不存在等等。
500-599表示服务器错误。

一旦出错(Status Code=4xx),应该返回出错信息,通常使用下面的格式,让API的请求端可以利用(如弹出框提示出错信息):

1
2
3
{
error: "Invalid API key"
}

响应数据

根据请求的Resource和动词的不同,响应数据的内容也会不同:

1
2
GET /collection
返回数组

1
2
GET /collection/resource
返回单个resource对象
1
2
POST /collection
返回新生产的resource对象
1
2
PUT /collection/resource
返回完整的resource对象
1
2
PATCH /collection/resource
返回完整的resource对象
1
2
DELETE /collection/resource
返回空

Protocol

为了安全,应当只使用HTTPS协议。

Authentication

身份认证就拥抱OAuth 2.0框架(e.g. Facebook)吧,这块了解不多就不瞎说了。

Hypermedia API

Github的API就是这种新套路,了解得不够深,不敢妄言。

Reference

Principles of Good RESTful API Design - Thomas Hunter II
理解RESTful架构 - 阮一峰