# RESTful API

自从 Roy Fielding 博士在 2000 年他的博士论文中提出 REST(Representational State Transfer)风格的软件架构模式后,REST 就基本上迅速取代了复杂而笨重的 SOAP,成为 Web API 的标准了。

REST 就是一种设计 API 的模式。最常用的数据格式是 JSON。由于 JSON 能直接被 JavaScript 读取,所以,以 JSON 格式编写的 REST 风格的 API 具有简单、易读、易用的特点。

Rest 架构的主要原则:

  • 网络上的所有事物都被抽象为资源

  • 每个资源都有一个唯一的资源标识符

  • 同一个资源具有多种表现形式(xml,json 等)

  • 对资源的各种操作不会改变资源标识符

  • 所有的操作都是无状态的

  • 符合 REST 原则的架构方式即可称为 RESTful

# 1. 以资源为中心的 URL 设计

资源 是 Restful API 的核心元素,所有的操作都是针对特定资源进行的。而资源就是 URL(Uniform Resoure Location)表示的,所以简洁、清晰、结构化的 URL 设计是至关重要的。

我们可以看到几个特性:

  • 资源分为单个文档和集合,尽量使用复数来表示资源,单个资源通过添加 id 或者 name 等来表示

    /students
    /students/9527
    
  • 资源可以嵌套,通过类似目录路径的方式来表示,以体现它们之间的关系

    /students/9527/teachers
    /teachers/10086/courses
    
  • 一个资源可以有多个不同的 URL,即,多个不同的 URL 可能/可以指向同一个资源。

    /students/9527/dormitory
    /dormitory/3306
    
  • 理论上,URL 应该是大小写敏感的。所以为了避免歧义,尽量使用 小写 字母。

另外,理论上只有 id 这样具有唯一性的标识才能【嵌】在 URL 中。

# 2. CRUD 操作请求

通过 HTTP 的请求方式,来表示你是要对资源作何种操作。

请求方式 描述
GET 获取资源
POST 创建资源
PUT 更新资源
DELETE 删除资源
  • 例如:

    GET /students
    
    GET /teachers/10086/courses 
    
    POST /students
    
    PUT /students/9527
    
    DELETE /students/9527
    
  • 反例:

    /getAllCars
    /createNewCar
    /deleteAllRedCars
    

理论上 URL 中 不应该 出现动词。

# 3. 特殊情况,特殊处理

在实际资源操作中,总会有一些不太贴合【增删改查】这四个动作的动作。

比如一个博客网站,会有把写好的文章【发布】的功能。

可以有以下三种方案:

# 破例添加动词

允许在 URL 的末端添加一个动词,并使用 POST 方式提交请求。比如 :

操作 URL
发布一篇文章 POST /articles/<id>/publish
重新发送邮件 POST /mails/<id>/resend

# 将动作转变为资源的状态改变

因为 发布 操作,博客的状态就分为 已发布未发布 。因此,发布操作,实际上就是将博客的状态变为已发布状态。

在文章中增加布尔类型的 published 属性,发布的时候就是更新该字段 PUT /articles/<id>?published=true

# 把动作转换成另一种资源

把动作转换成某种可以执行 CRUD 操作的资源, github 就是用了这种方法。

比如,将『喜欢』这个概念体现为文章的 star 。

  • 『喜欢』一篇文章: PUT /article/<id>/star

  • 『取消喜欢』DELETE /article/<id>/star

# 4. 分页查询及返回

# 分页请求参数

当要进行分页查询时,在请求原有 API 的基础上,可额外附带如下(或类似参数)两个参数:

参数 含义
page_number 表示请求第几页,默认为 1
page_size 表示每页最多显示多少条记录,默认为系统默认值

你也可以将普通查询和分页查询统一起来。即,认为所有的查询都认为是分页查询,在客户端没有提供这两个参数时,认为它俩是默认值。例如 (1, 10)

# 分页返回

返回资源集合时,包含于分页有关的数据。例如:

{
  "page_number": 1,     # 当前是第几页
  "page_size": 10,      # 每页多少数据
  "pages": 3,           # 总共多少页
  "has_next": true,     # 是否有下一页数据
  "has_prev": false,    # 是否有前一页数据
  "total": 27,          # 总共多少数据
  "data": [             # 查询结果
    { ... },
    { ... },
    { ... }
  ]
}

# 排序参数(了解)

其实在前后端分离的项目中(Restful 的最常用最典型的使用场景),排序这个工作是由前端(JS)来负责进行处理的,后台只需要返回数据集即可。至于页面上根据哪个属性排序,如何排序(升降序),后台是不需要考虑这些的。

不过,如果某些情况下,要求由后端排序,由后端返回排序后的数据。这时,可以约定前端请求时,发送类似如下参数:

?sortby=<name>&order=<asc>

指定返回结果按照哪个属性排序,以及排序顺序。

# 5. Restful 请求返回的状态码

当后台返回数据时,如何设置 HTTP 响应请求状态码,有两大『流派』:

# 重用 HTTP 响应状态码

这个流派的人认为:HTTP 协议有一套现成的响应状态码,更重要的是随着 HTTP 协议的广泛使用,它已经从『知识』变成了『常识』,而被广泛接收。例如,即便不是程序员,也知道 404 意味着什么。既然如此,那就 直接使用 HTTP 响应协议的状态码及其含义

HTTP 响应状态码大体上:

  • 2xx 表示成功;

  • 4xx 表示由于客户端的原因,导致该请求失败(客户端做出修正后,在其请求有可能成功。例如,参数格式错误。)

  • 5xx 表示由于服务端的原因导致该请求失败(客户端再次请求,仍有可能失败。例如,后台数据库宕机了。)

因此 Restful 返回的状态可按如下规则返回:

操作 状态 返回码 信息
POST 成功 201 CREATED
DELETE 成功 204 NO CONTENT
PUT 成功 200 SUCCESS
GET 成功 200 SUCCESS
失败 404 NOT FOUND
任何请求 参数校验失败 400 BAD REQUEST
任何请求 没有通过身份认证 401 NOT AUTHORIZED
任何请求 权限不够 403 FORBIDDEN
任何请求 服务器内部错误 500 Internal Server Error

# 自定义状态码

这个流派的认为『重用流』有几个缺陷:

  1. HTTP 响应码有限。成功时返回 200 倒好说。但是问题是失败时,失败错误码远小于业务逻辑上出错的可能。此时,用一个错误码代表一堆错误原因,并不精准。

  2. 404 这样的错误码是人尽皆知,但是不能保证前后端程序员对其他错误码的认知和使用场景也很清楚。这种情况下,什么错误返回什么状态码?这就加大了前后端开发人员的沟通成本。

因此,这个流派的人认为应该这么返回状态码:

  1. 无条件返回 200 。这样前端开发人员就不用纠结应该走哪个返回码的判断逻辑。但这个 200 不代表请求一定就是成功的,这要接着看第二条。

  2. 返回的数据中,自带自定义的状态码(和对应的状态信息),例如:{ code: 9527, msg: 'xxxx', data: ... } 。前端开发人员靠这里的 code 的值作为标准,判断请求是否成功,而非 HTTP 请求的那个 200 。

这里两大『流派』信众众多,(暂时)不分高低。

  • 相较而言,『重用流』对程序员要求更高(需要熟知 HTTP 协议)

  • 『自定义流』虽然破坏了 200 状态码的本意,但是对开发人员更友好,也更灵活。

# 6. 其他

# 版本

在 url 中指定 API 的版本是个很好地做法。如果 API 变化比较大,可以把 API 设计为子域名。

https://api.example.com/v{n}/...

# 分隔符

RESTful API 应具备良好的可读性,当 URL 中某一个片段(segment)由多个单词组成时,建议使用 - 来隔断单词,而不是使用 _