RESTful API 设计指南(4)——如何实现RESTful API(上)

236 阅读11分钟

前言

在前面一系列的文章中,我们介绍了 RESTful API 出现的背景,以及为什么要引入 RESTful API 风格。那么从本章开始,我们将进入实现篇,在本篇中,我们主要关注如何实现 RESTful API 风格。

什么是 RESTful API 风格?在 RESTful API 设计指南(1)——开篇词 中,我们进行了总结:

只要你的 HTTP 接口使用 POST/DELETE/PUT/GET 代表增删改查操作,使用 HTTP 状态码代表结果,使用 URI 代表操作对象,你的 API 就是 RESTful API

所以,其背后是一个对 HTTP 协议进行再学习的过程,本质上就是要用好 HTTP 协议:

  • 设计好 URL 和 HTTP 主体(path和body):充分利用 URL 的 path 和 query 部分,path 中代表要操作的资源(对象),query放查询条件,body中放请求参数和响应结果
  • 正确选择 HTTP 方法(method):平常你可能只使用 GET 和 POST 方法,但是 HTTP 协议中还提供了 DELETE、PUT 和 OPTION 等方法。所以,REST 风格中就充分利用了 HTTP 应用层协议的特性,把增删改查分别对应到 HTTP 的 POST、DEELTE、PUT和GET方法上。
  • 正确选择 HTTP 状态码(status code):正确的使用状态码,当因为客户端参数异常而发生错误,应当返回 400 Bad Request,如果是服务端内部原因发生错误,应当返回 500 Internal Server Error,如果修改成功,通常情况下应当返回 204 No Content。
  • 充分利用 HTTP 报头(request header):一些通用的参数应当优先考虑放在 HTTP 报头中,比如接口认证,每个接口都需要携带 token,可以使用 Authorization 这个报头。

下面,让我们再复习一下 HTTP 协议的这几个关键部分。

URL

URL 是 URI 的一个子集,特指 HTTP 协议中的资源定位符,URI格式定义如下:

                    hierarchical part
        ┌───────────────────┴─────────────────────┐
                    authority               path
        ┌───────────────┴───────────────┐┌───┴────┐
  abc://username:password@example.com:123/path/data?key=value&key2=value2#fragid1
  └┬┘   └───────┬───────┘ └────┬────┘ └┬┘           └─────────┬─────────┘ └──┬──┘
scheme  user information     host     port                  query         fragment

根据 RFC 3986 文档:

其中,我们需要重点关注 path 和 query 2个部分,正如开头所说,HTTP是应用层协议,要实现 RESTful API 的关键就是,我们要充分利用其应用层协议的特性,不要重复造轮子。

path

在 RESTful API 中,我们用 path 部分表示我们要操作的资源,资源名通常是名词形式,且用复数形式代表一个集合。如果是单个集合,则使用 {id} 形式表示。

path 具有层级结构,指向的资源拥有从属关系。比如在 www.example.com/images/phot… 的 URL 中,photo.jpg 是属于 images 这个目录的,同样的,images 这个目录下可能还有 photo1.jpg、photo2.jgp、…… 和 photoN.jgp 等文件。

所以,在 RESTful API 中,我们可以利用"父子"关系的这种特性来区分操作的是集合还是单个对象。比如 POST /users 代表要往一个集合中新增元素(用户),DELETE users/1 代表要删除单个用户。其中 users/{id} 就属于父子关系,即 id 指向的单个用户是从属于整体用户的。

除了父子关系之外,还有一种"并列"关系,父子关系使用 "/" 分割,并列关系使用 "," 隔开。

并列关系的设定,通常出现在 PUT 更新操作中,如 PUT /users/{id}/name,age,sex 代表要更新用户的部分属性,这里只更新用户的昵称、年龄和性别,而这3个属性之间就是并列关系,但都从属于单个用户,所以和 {id} 之间用 "/" 隔开。

query

查询参数,kv形式,多个参数之间通过 & 进行拼接。

以查询用户详细信息为例,在传统的 HTTP API 中,我们会把用户 id 放在 query 中: GET /queryUserInfo?userId=1

但是在 RESTful API 中,id 是放在 path 中的。如下图,其中 :id 代表了占位符,具体的值在 Path Variables 中指定:

image.png

一方面,这里也是父子关系的体现,即用户 33 属于 users 这个集合。另一方面,相比传统 API,RESTful API 中 path 标志了一个唯一的资源,/users/33 永远只会出现一个且始终指向同一个用户。

HTTP Method

传统 API 只使用 GET 和 POST 2种方法,而 RESTful API 使用 4 种方法。其中 GET 和 POST 的定义首次出现在 HTTP 1.0 (RFC 1945)中,PUT 和 DELETE 出现在间隔几个月后发布的 HTTP 1.1 (RFC 2616)版本中。

其定义如下:

  • GET:表示检索信息,允许缓存。可以请求任意资源,如 HTML、图像或者视频、JSON文件、XML文件和CSS文件等等,是一种安全操作,意味着它不应该改变服务器上任何资源的状态
  • POST:将数据发送到服务器进行处理,不允许缓存。数据格式可以是 xml、json等。POST操作是不安全的,因为它有能力更新服务器状态,并且在执行时对服务器的状态造成潜在的副作用。
  • PUT:用于替换给定URL标识的资源,如果资源已存在,则进行修改,应响应200(OK)或者204(No Content),否则创建资源,返回 201(Created)。PUT操作是不安全的,可以改变服务器资源的状态。但它是幂等的,调用100次 PUT 操作将航班状态设置为准时和调用一次 PUT 操作的效果是一样的,航班最终的状态都是准时。
  • DELETE:用户删除给定URL标志的资源,如果执行成功且响应中包含说明数据,应响应 200(OK),否则应响应 204(No Content),和PUT操作一样,DELETE方法是幂等但是不安全的。

从 HTTP 协议规范中,我们可以发现 GET、POST、PUT 和 DELETE 刚好对应查、增、改、删操作,也就是我们说的增删改查,所以我们可以利用 HTTP Method 代表 API 的动作。这也是为什么传统 API 需要在 path 中声明动作的原因。而 RESTful API 不需要,还是那句话:充分利用 HTTP 应用层协议的特性!

在 RESTful API 中,Method 通常搭配 path 使用,如果按照面向对象的思想,我们可以把 path 看成对象名,把 HTTP method 看成方法,则他们可能的组合如下:

  • POST /users:新增一个用户,面向对象表示为 users.add(info)
  • DELETE /users/{id}:删除一个具体的用户,面向对象表示为 users.deleteById(id)
  • PUT /users/{id}:修改某个用户的信息,具体的信息放在请求体中,面向对象表示为 users.updateById(id, info)
  • GET /users?offset=10&limit=100:查询所有用户列表,查询条件放在 query 中,面向对象表示为 users.getAll(offset, limit)
  • GET /users/{id}:某个某个具体的用户信息,面向对象表示为 users.getByUserId(id)

HTTP Status Code

在 RFC 2616 中规定了服务器响应HTTP请求的消息格式,由三个主要部分组成:状态行(Status-Line)、响应头(response-header)以及消息体(message-body,对于某些响应可能为空):

Response      = Status-Line
                *(( general-header
                 | response-header
                 | entity-header ) CRLF)
                 CRLF
                 [ message-body ]

其中 Status Code 代表状态码,出现在 HTTP 响应的起始行(Status-Line)部分,提供了关于响应的基本信息,并总是包含以下三个元素,以空格分隔:

  • HTTP版本:例如 HTTP/1.1
  • 状态码(Status Code):一个三位数字,用于描述请求的结果。例如,200表示成功,404表示未找到资源。
  • 状态消息(Status Message):一个简短的文本描述,对应于状态码。例如,对于状态码200,状态消息是"OK"。

如下一个典型的 HTTP 响应起始行中,200 就是 Status Code,代表服务器已成功处理该请求:

HTTP/1.1 200 OK

状态码是 HTTP 协议的核心组成部分,用于指示服务器对请求的处理结果。它们被分为五类,范围从100到599,每类都代表了不同种类的响应。例如,1xx类表示信息性响应,2xx类表示成功,3xx类表示重定向,4xx类表示客户端错误,而5xx类表示服务器错误。

在一些不规范的API设计中,对于 Status Code 的处理,可能会永远返回 200,具体的请求结果被封装在了 Body 中:

HTTP/1.1 200 OK 
Content-Type: application/json 
Content-Length: 123 
{
    "errorCode": 0,
    "errorMsg": "success",
    "data": {
        "userId": 123,
        "username": "john_doe",
        "email": "john.doe@example.com",
        "isAdmin": false,
        "createdAt": "2023-04-01T12:00:00Z"
    }
}

客户端通过解析 "errorCode" 来判断请求成功还是失败,通过 "errorMsg" 来给出错误提示,可能会定义如下 errorCode:

  • 0:成功
  • 4000001:参数错误
  • 4000003:权限不足
  • 4000004:用户不存在
  • ……

随着功能的增加,系统中错误码可能越来越多:

  • 如果要判断某一类错误,比如参数异常导致的错误或者属于某个服务返回的错误,则我们必须对错误码进行分组。比如 4000000 - 4100000 代表服务A的错误码范围或者代表参数错误。
  • 引入微服务架构后,各个服务之间错误码可能出现重复。所以需要维护一个全局的仓库?每次新增错误码都需要小心翼翼的查阅,应该取多少值合适。
  • 错误码太多了,维护成本越来越高,查找也越来越困难,什么情况下会返回这个错误码?是因为什么原因?
  • 跨团队协作时,大家习惯不一致。可能有些人对成功的错误码定义成0,有的又定义成一个固定的数字,甚至有的直接使用字符串 "success" 返回,导致服务对接比较痛苦

等等这些问题都要考虑,但是如果我们回顾 RFC 文档中对 Status-Code 的定义,如上的问题其实我们也就解决了大半:

  • 2xx:代表成功,不管是任何服务,都按此规范实现API行为。比如 200 OK 通常并表示成功并且有 Body 响应体,201 Created 表示创建成功,204 Not Conent 表示没有响应数据,如修改成功。
  • 4xx:表示由客户端导致的错误。如果是参数类异常,就返回 400 Bad Request;是权限不够,就返回 403 Forbidden。要查询的用户不存在,就返回 404 Not Found。
  • 5xx:表示由服务端导致的错误。如调用第三方接口超时,数据库异常,内部崩溃错误或者触发了限流等等。

如下是一些在 RESTful API 中常用的 Status-Code:

| "200"  ; Section 10.2.1: OK
| "201"  ; Section 10.2.2: Created
| "203"  ; Section 10.2.4: Non-Authoritative Information
| "204"  ; Section 10.2.5: No Content
| "400"  ; Section 10.4.1: Bad Request
| "401"  ; Section 10.4.2: Unauthorized
| "402"  ; Section 10.4.3: Payment Required
| "403"  ; Section 10.4.4: Forbidden
| "404"  ; Section 10.4.5: Not Found
| "408"  ; Section 10.4.9: Request Time-out
| "409"  ; Section 10.4.10: Conflict
| "500"  ; Section 10.5.1: Internal Server Error
| "501"  ; Section 10.5.2: Not Implemented
| "502"  ; Section 10.5.3: Bad Gateway
| "503"  ; Section 10.5.4: Service Unavailable

在 RFC 7231 中,对这些状态码进行了更详细的描述,有兴趣可以阅读一下。

总结

实现 RESTful API 的关键是用好 HTTP 协议:

  • 设计好 URL 和 HTTP 主体
  • 正确选择 HTTP 方法
  • 正确选择 HTTP 状态码
  • 充分利用 HTTP 报头

所以本章对 HTTP 协议中的 path、method 和 status code 进行了简单的回顾,然而 HTTP 协议的内容远远不止于此,比如HTTP报头和HTTP主体这里并没有深入介绍,相关内容需要读者更深入的学习。

下一章,我们将使用 go + gin 实现一个简单的 RESTful API 服务,从代码层面,来进一步学习如何实现 RESTful API。

参考:


作者简介:一线Coder,公众号《Go和分布式IM》运营者,开源项目: CoffeeChat 、interview-golang  发起人 & 核心开发者,终身程序员。