关于 RESTful API 设计的总结

为什么要用 RESTful

RESTful 给我的最大感觉就是规范、易懂和优雅,一个结构清晰、易于理解的 API 完全可以省去许多无意义的沟通和文档。并且 RESTful 现在越来越流行,

在开始介绍 RESTful API 之前,先讲一下 RESTful 架构。

RESTful 架构

REST,即Representational State Transfer 的缩写。意为 " 表现层状态转化 " 。

要理解RESTful架构,最好的方法就是去理解 Representational State Transfer 这个词组到底是什么意思,它的每一个词代表了什么涵义。如果把这个名称搞懂了,也就不难体会 REST 是一种什么样的设计。

资源 (Resources)

REST的名称"表现层状态转化"中,省略了主语。"表现层"其实指的是"资源"(Resources)的"表现层"。

所谓"资源",就是网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的实在。你可以用一个 URI(统一资源定位符)指向它,每种资源对应一个特定的 URI 。要获取这个资源,访问它的 URI 就可以,因此 URI 就成了每一个资源的地址或独一无二的识别符。所谓"上网",就是与互联网上一系列的"资源"互动,调用它的 URI 。

表现层(Representation)

"资源"是一种信息实体,它可以有多种外在表现形式。我们把"资源"具体呈现出来的形式,叫做它的"表现层"(Representation)。
比如,文本可以用 txt 格式表现,也可以用 HTML 格式、 XML 格式、JSON 格式表现,甚至可以采用二进制格式;图片可以用 JPG 格式表现,也可以用 PNG 格式表现。
URI 只代表资源的实体,不代表它的形式。严格地说,有些网址最后的" .html "后缀名是不必要的,因为这个后缀名表示格式,属于"表现层"范畴,而 URI 应该只代表"资源"的位置。它的具体表现形式,应该在 HTTP 请求的头信息中用 Accept 和 Content-Type 字段指定,这两个字段才是对"表现层"的描述。

状态转化(State Transfer)

访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,势必涉及到数据和状态的变化。
互联网通信协议 HTTP 协议,是一个无状态协议。这意味着,所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生"状态转化"(State Transfer)。而这种转化是建立在表现层之上的,所以就是"表现层状态转化"。
客户端用到的手段,只能是HTTP协议。具体来说,就是HTTP协议里面,四个表示操作方式的动词:GET 、 POST 、 PUT 、 DELETE 。 它们分别对应四种基本操作: GET 用来获取资源, POST 用来新建资源,PUT 用来更新资源,DELETE 用来删除资源。

综述

总结一下什么是RESTful架构:

  • 每一个URI代表一种资源;
  • 客户端和服务器之间,传递这种资源的某种表现层;
  • 客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。

RESTful API 的设计

介绍完 RESTful 的含义,再说说 RESTful API 的设计。

协议

如果能全站 HTTPS 当然是最好的,不能的话也请尽量将登录、注册等涉及密码的接口使用 HTTPS。

域名

应该尽量将API部署在专用域名之下。如:

https://api.example.com

如果确定API很简单,不会有进一步扩展,可以考虑放在主域名下。

https://example.org/api/

版本号

API 的版本号和客户端 APP 的版本号是毫无关系的,不要让 APP 将它们用于提交应用市场的版本号传递到服务器,而是提供类似于v1、v2之类的 API 版本号。

版本号拼接在 URL 中。如:

api.example.com/v1/users

或是放在 Header 中:

api.example.com/users

version=v1

请求

一般来说 API 的外在形式无非就是增删改查(当然具体的业务逻辑肯定要复杂得多),而查询又分为详情和列表两种,在 RESTful 中这就相当于通用的模板。例如针对文章(Article)设计 API,那么最基础的 URL 就是这几种:

  • GET /articles: 文章列表
  • GET /articles/id:文章详情
  • POST /articles/: 创建文章
  • PUT /articles/id:修改文章
  • DELETE /articles/id:删除文章

Token 和 Sign

API 需要设计成无状态,所以客户端在每次请求时都需要提供有效的 Token 和 Sign,在我看来它们的用途分别是:

  • Token 用于证明请求所属的用户,一般都是服务端在登录后随机生成一段字符串(UUID)和登录用户进行绑定,再将其返回给客户端。Token 的状态保持一般有两种方式实现:一种是在用户每次操作都会延长或重置 TOKEN 的生存时间(类似于缓存的机制),另一种是 Token 的生存时间固定不变,但是同时返回一个刷新用的 Token,当 Token 过期时可以将其刷新而不是重新登录。
  • Sign 用于证明该次请求合理,所以一般客户端会把请求参数拼接后并加密作为 Sign 传给服务端,这样即使被抓包了,对方只修改参数而无法生成对应的 Sign 也会被服务端识破。当然也可以将时间戳、请求地址和 Token 也混入 Sign,这样 Sign 也拥有了所属人、时效性和目的地。

业务参数

在 RESTful 的标准中,PUT 和 PATCH 都可以用于修改操作,它们的区别是 PUT 需要提交整个对象,而 PATCH 只需要提交修改的信息。但是在我看来实际应用中不需要这么麻烦,所以我一律使用 PUT,并且只提交修改的信息。

另一个问题是在 POST 创建对象时,究竟该用表单提交更好些还是用 JSON 提交更好些。其实两者都可以,在我看来它们唯一的区别是 JSON 可以比较方便的表示更为复杂的结构(有嵌套对象)。另外无论使用哪种,请保持统一,不要两者混用。

还有一个建议是最好将过滤、分页和排序的相关信息全权交给客户端,包括过滤条件、页数或是游标、每页的数量、排序方式、升降序等,这样可以使 API 更加灵活。但是对于过滤条件、排序方式等,不需要支持所有方式,只需要支持目前用得上的和以后可能会用上的方式即可,并通过字符串枚举解析,这样可见性要更好些。例如:

搜索,客户端只提供关键词,具体搜索的字段,和搜索方式(前缀、全文、精确)由服务端决定:

/users/?query=ScienJus

过滤,只需要对已有的情况进行支持:

/users/?gender=1

分页:

/users/?page=2&per_page=20

响应

尽量使用 HTTP 状态码,常用的有:

  • 200:请求成功
  • 201:创建、修改成功
  • 204:删除成功
  • 400:参数错误
  • 401:未登录
  • 403:禁止访问
  • 404:未找到
  • 500:系统错误

但是有些时候仅仅使用 HTTP 状态码没有办法明确的表达错误信息,所以也可以在里面再包一层自定义的返回码,例如:

成功时:

{
    "code": 100,
    "msg": "成功",
    "data": {}
}
{
    "code": -1000,
    "msg": "用户名或密码错误"
}

data是真正需要返回的数据,并且只会在请求成功时才存在,msg只用在开发环境,并且只为了开发人员识别。客户端逻辑只允许识别code,并且不允许直接将msg的内容展示给用户。如果这个错误很复杂,无法使用一段话描述清楚,也可以在添加一个doc字段,包含指向该错误的文档的链接。

返回数据

JSON 比 XML 可视化更好,也更加节约流量,所以尽量不要使用 XML。

创建和修改操作成功后,需要返回该资源的全部信息。

返回数据不要和客户端界面强耦合,不要在设计 API 时就考虑少查询一张关联表或是少查询 / 返回几个字段能带来多大的性能提升。并且一定要以资源为单位,即使客户端一个页面需要展示多个资源,也不要在一个接口中全部返回,而是让客户端分别请求多个接口。

最好将返回数据进行加密和压缩,尤其是压缩在移动应用中还是比较重要的。

当才华撑不起野心时,就静下来学习。