浅淡RESTful API设计规范

目前主流的通讯协议主要有RPC、http/1.1、http/2等,而http中最主流的无疑就是restful了,由于工作的原因,经常需要和不同的外部服务商进行系统集成,给出的文档都说是基于restful规范设计,遗憾的是,在我看来,几乎没有看到过真正可以称之为restful架构的api设计。今天就来谈谈如何设计一个规范、优雅、可读性高的restful api。

关于restful设计的最佳实践,这里还是推荐阮一峰老师的RESTful API 设计指南,个人觉得是国内范围里讲的最好的了。rest架构,从个人角度理解,核心做了两件事情:

  • 资源定位
  • 资源操作

其实从REST的定义中就能看出来,表述层对应的就是描述资源的位置(资源定位),状态转移就是对资源的状态进行变更操作(增删改查)。
RESTful HTTP动词

下面举个实际的例子:假设我们数据库里有一张User表,我们根据表建好了领域对象模型User,按照restful规范设计的接口应该是这样的:

  • 新增用户,[POST] /users

  • 修改用户,[PUT] /users/id

  • 删除用户,[DELETE] /users/id

  • 查找全部用户,[GET] /users

看到这里可能有同学就要问了,干嘛非得这么设计,还要用什么http动词,delete、put神马的我都没用过,平时都是get、post走天下,也用的好好的呀。新增用户不能用/addUser吗?删除用户不能用/deleteUser吗?感觉也很清楚啊(事实上很多公司的所谓的restful接口文档都是这么定义的)好,现在让我们回到前面,复习一下rest的定义,第一条叫做资源定位,如果还不理解,那让我们再想想URL的定义,叫做统一资源定位符,也就是说url是用来表示资源在互联网上的位置的,所以说在url中应该包含动词,只能包含名词,对资源的操作应该体现在http method上面。

如果这样理解还比较抽象的话,这里不妨再打一个比方,比如在jane的网站有一张小汽车的图片,地址是http://jane.com/img/car.jpg,现在jane想设计一个api接口,实现对这张图片的删除操作,这个api应该怎么设计?根据rest的设计规范,很容易得出是

  • [DELETE] http://jane.com/img/car

非常的清晰明了(这里暂时先不考虑调用方是否有权限删除服务器上的资源)
注意这里为了讲述原理没有加资源的后缀.jpg,引用阮一峰老师的话

严格地说,有些网址最后的".html"后缀名是不必要的,因为这个后缀名表示格式,属于"表现层"范畴,而URI应该只代表"资源"的位置。它的具体表现形式,应该在HTTP请求的头信息中用Accept和Content-Type字段指定,这两个字段才是对"表现层"的描述。

在这个例子里,我们可以通过在http header里指定content-type为image/jpeg来申明这个资源是一张jpg格式的图片
接下来,如果要查询(获取)这张图片呢?自然就是

  • [GET] http://jane.com/img/car

再进一步,我们再改造一下这个api,加上.jpg后缀,这个api就变成了

  • [GET] http://jane.com/img/car.jpg

看到这里大家应该都很熟悉了,这就是我们每天上网要进行无数次操作的api,就是这么设计出来的。

我们继续扩展一下,现在我们要获取的不是静态图片资源了,而是一辆小汽车的相关信息,并且需要对车库里的汽车进行增删改查的维护操作。如果用上面讲的那种一般http的写法,可能会写出类似下面这样的api(只用GET和POST方法)

[POST] http://jane.com/garage/addCar
body:{"brand":"ford","model":"focus","price":"120000"}

[POST] http://jane.com/garage/udpateCar?id=123
body:{"brand":"ford","model":"focus","price":"130000"}

[GET] http://jane.com/garage/queryCarList

[GET] http://jane.com/garage/queryCarSingle?id=123

[GET] http://jane.com/garage/deleteCar?id=123

看出问题来了吗?一个严重的问题是url丢失了资源的位置,更重要的是,你可以叫deleteCar,也可以叫eraseCar,还可以叫removeCar,具体什么含义只有设计这个api的人才能说清楚。而如果用http method,那就肯定是DELETE这个方法,所有看这个api的人都知道你提供的是一个删除这个资源的方法,这就叫做语义化,能用最少的话把一个意思表达清楚,这本身就是一种优雅的设计方式。使用rest设计上述api,结果如下:

[POST] http://jane.com/garage/cars
body:{"brand":"ford","model":"focus","price":"120000"}

[PUT] http://jane.com/garage/cars/123
body:{"brand":"ford","model":"focus","price":"130000"}

[GET] http://jane.com/garage/cars

[GET] http://jane.com/garage/cars/123

[DELETE] http://jane.com/garage/cars/123

这里http://jane.com/garage/cars/123代表了id为123的这辆小汽车在网上的唯一位置,本质上和http://jane.com/img/car所代表的含义是一样的。使用rest能带来的额外的好处,是你可以做很方便的权限控制。因为POST、PUT、DELETE、GET等都是标准的http方法,你可以很轻松的在nginx这样的7层代理或者防火墙上设置策略,禁止某些资源的修改及删除操作,而这显然是自定义的url所达不到的。

除了HTTP METHOD,rest另外一套重要的规范就是HTTP STATUS,这套状态码规范定义了常规的api操作所可能产生的各种可能结果的描述,遵循这套规范,会使得你的api变得更加可读,同时也便于各种网络、基础设施进行交易状态监控。经常会用到的status code整理如下:

200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
204 NO CONTENT - [DELETE]:用户删除数据成功。
400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。

事情到了这里似乎一切都很美好,可惜人生不如意十之八九,api设计也不可能一帆风顺。总有一些场景是CRUD所抽象不了的,举个简单的例子,用户登陆,如何去匹配CRUD模型?这里我的建议是,先把你的操作对象或者行为抽象为资源,然后就简单了,无非就是对这个资源的CRUD。
针对用户登陆这个场景,我们可以把用户在远程服务器的会话信息抽象为一个资源,这样的话,登陆其实就是在远程服务器增加了一个会话资源,不难想到,登出就是在远程服务器删除了一个会话资源,所以api可以这样设计

[POST] /login
[DELETE] /logout

如果是发送短信呢?似乎更难。。。这里再次请出阮一峰老师

如果某些动作是HTTP动词表示不了的,你就应该把动作做成一种资源。比如网上汇款,从账户1向账户2汇款500元,正确的写法是把动词transfer改成名词transaction,资源不能是动词,但是可以是一种服务

这样的话你把发送短信理解成一种服务,api可以这样设计

[POST]  /smsService
body:{"mobile":"13813888888","text":"hello world"}

最后建议大家去看一下github的api文档,可以说是restful架构最完整的实现了,看完后一定会对restful规范有着更深入的理解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章