从事Web开发的童鞋对RESTful API很多人都不陌生,不同项目的接口形式千奇百怪,有些声称使用RESTful,但是实际上并不完全符合规范,不禁让我思考,到底要不要使用RESTful API的风格?
举个例子,相信大家见过不少下面风格的API:
用途 | 请求方法 | 命名 |
---|---|---|
获取用户列表 | GET | /getUserList |
获取单个用户信息 | GET | /getUserInfo?id=1 |
新增用户 | POST | /createUser |
更新用户 | POST | /updateUser |
删除用户 | POST | /deleteUser |
这是一个最简单的CURD接口,说实话,我并不反对这种形式的API,虽然说看上去俗了点,但是至少见名知意,除了获取数据使用GET,其它一律POST,简单粗暴,没什么大问题。
如果展开说,上面的这种形式在复杂的情况下,无法更好的表达接口的含义,比如说假设你要写一个获取用户的好友列表接口,那可能就得这么写:
1 | 获取用户好友列表 GET /getUserFriends?user_id=1 |
这种命名方式在复杂情况下会导致接口名字越来越长。。。可读性就变的差了,而且一眼看不出来数据之间的关系,不好分类,假设有些人名字写的差就更难解读了。
如果使用RESTful则可以采用这种形式:
1 | 获取用户好友列表 GET /users/:id/friends |
传统的接口采用动词+接口名称的形式来表达含义,而RESTful推荐使用HTTP请求方式来定义,比如GET用于获取数据、POST用于创建、PUT用于修改、DELETE用于删除。
上面这些接口如果使用RESTful形式改写就是这样:
用途 | 请求方法 | 命名 |
---|---|---|
获取用户列表 | GET | /user |
获取单个用户信息 | GET | /user/:id |
新增用户 | POST | /user |
更新用户 | PUT | /user/:id |
删除用户 | DELETE | /user/:id |
关于是用user还是users这块也有一个分歧,有些人觉得应该是用复数,因为有多个用户,所以获取用户得用getUsers,这个问题同样存在数据表名称上面。
我觉得接口用不用复数取决于数据表名称用不用,如果数据表就是users,那接口最好也用,保持一致。
至于说数据表名称该不该用复数,很多人都有不同的看法,在项目开始的时候统一明确的一个规范就行了,如果你要问我,我建议使用单数。
除此之外,RESTful还推荐通过HTTP状态码来区分结果,比如200表示正常,403表示没有权限,传统的接口往往统一使用200,然后在响应内容里面去区分。
简单说,RESTful的设计初衷是把这些数据实体当成一种资源,通过URL去定位资源,用HTTP描述操作,达到以下几个目标:
- 看URL就知道要什么
- 看HTTP方法就知道干什么
- 看HTTP状态码就知道结果如何
在我的职业生涯里面见过很多看似的RESTful的风格,但是实际上也不完全是,比如说PUT、DELETE的时候id是以http query string的方式传递,而不是path:
1 | 删除用户 DELETE /user?id=1 |
还有接口是直接把所有参数放到body里面传递,虽然说这些方式都可以,也不麻烦,因为现在很多Web框架都这些参数的处理都很方便,但是感觉还是不伦不类,当然有时候也是逼不得已,比如有些表的设计没有单一id主键,只有一个复合主键,这就很难使用path这种形式。
并非完美
RESTful的设计规范很理想,但是并不适合所有的项目,因为很多接口不是纯粹的CURD操作,它不仅仅是对一张表的增删改查,这导致很难通过URL去表达含义。
对于单纯的获取数据、修改数据的接口RESTful比较合适,但是当你的接口里面包含非常复杂的业务逻辑的时候,RESTful就非常麻烦了,根本无处下手。
举个例子,一个根据用户的住址获取附近的超市的接口,这样的接口如果使用传统的方式可能是这样:
1 | /getUserNearbySupermarket?user_id=1 |
如果采用RESTful就存在一个问题,这到底是围绕着用户还是超市来设计呢?
1 | /user/:id/nearby_supermarket |
以上这3个例子你们觉得哪个好?对此我也纠结了很久,最后在参考了很多云厂商的API之后,个人觉得第一种比较合适。
有些时候RESTful API定义的GET、POST、PUT、DELETE4个常用方式并不能满足所有的应用场景,或者不够清晰,比如说业务有个场景是要清空用户黑名单,这属于PUT还是DELETE?
从用户的角度来说说PUT,因为我是修改了用户的附属属性,但是从黑名单的角度是删除了数据,都有一定的道理。
或许,我们可以这么设计
1 | POST /user/:id/blacklist/empty |
我们可以认为blacklist是用户的属性,empty是一种动作,是不是更加清晰一点,至于方法是用POST还是PUT,区别都不大。
最佳实践
对于开放平台的接口,比如说一个云厂商,需要提供对应的sdk或者api给用户使用,推荐使用RESTful,实际上国外很多大厂的api是非常标准的RESTful风格,国内某大厂除外。
云服务本身就是提供资源,无论是服务器、IP、域名,它都是一种网络资源,而RESTful天生就是为了描述资源,并且云服务的操作逻辑简单,没有复杂的业务逻辑,对于服务器无法就是创建、查看、修改、删除、重启等。
以前用过vultr的API,就是非常规范的RESTful风格,看着很舒服:
用途 | 请求方法 | 命名 |
---|---|---|
获取实例 | GET | /instances |
获取单个实例 | GET | /instances/:id |
新增实例 | POST | /instances |
更新实例 | PUT | /instances/:id |
删除实例 | DELETE | /instances/:id |
启动实例 | POST | /instances/:id/start |
批量启动实例 | POST | /instances/start |
值得一说的是启动这个操作,它有2种形式,一个是操作单个实例,参数放在path里面,还有一个是批量操作,是有多个id,这时候参数是放在json body里面。
但是对于很多公司内部系统的接口,RESTful往往并不是最佳的选择,如果为了简单方便,不考虑这些乱七八糟的规范,我建议除了获取数据使用GET,其它一律使用POST。
用途 | 请求方法 | 命名 |
---|---|---|
获取用户列表 | GET | /user |
新增用户 | POST | /user |
更新用户 | POST | /user |
删除用户 | POST | /user |
比如上面这个写法,第一个GET接口要支持多个参数过滤,比如说可以这么写/user?id=1,相当于获取单个用户,除了GET之外的接口参数全部放到body里面,而且统一采用JSON形式。
还有一个响应状态码的问题,个人觉得还是统一200比较合适,在响应体内部另外定义code来表达结果,也就是说不依赖HTTP状态码,虽然有点不符合RESTful的规范,但是兼容性好,因为你不知道哪个中间件会怎么处理。
最后总结一下:
对于要不要使用RESTful风格的API这个问题没有明确的答案,如果你的应用逻辑简单,大多数是对一些数据表的增删改查,可以考虑使用RESTful风格的接口,相反如果你的应用业务逻辑复杂,也可以不用。
如果考虑到这是一个对外公开的OpenAPI,建议使用RESTful风格,毕竟这也算是一种业界共识,你整个什么getXXXByXXX显的确实有点low啊。
其实我们也可以2者混用,根据实际情况来决定,毕竟这只是一个接口路由,没必要纠结太多,大部分Web框架都支持各种形式的路由参数,对于简单的CURD可以使用RESTful,对于复杂的业务逻辑则特殊处理。