在本文中,将重点介绍一些实际设计RESTful API的建议,这些API是基于HTTP协议设计的。这些建议是参考网上一些好的资料并结合自己实际经验做了更加细致的分析,个人觉得有不错的参考价值。
GET /users - 获取用户列表
GET /users/:id - 获取用户ID号为:id的用户
POST /users - 创建新的用户
PUT /users/:id - 更新用户:id的信息
PATCH /users/:id - 更新:id指向的用户的部分信息
DELETE /users/:id - 删除:id指向的用户
其中,名词“users”用于标定用户集合这一资源;用户ID(:id)用于标定单个用户;用http规范的方法来表达语义。可以看到,变量:id出现在了url中,也就意味着REST服务需要对url进行解析,以获取该变量值。在前一篇博文中,我已经说明了不用PUT来新建资源的理由,因此这里不做展示。
如果换做是RPC实现,可能很简单的全部用POST的方法,然后在body中指定{:method :get-user :params ["user-id1"]}。
举个例子,一个用户可能有多个朋友,“朋友”就是一种关系,虽然在后台“朋友”和“用户”本身是都是存在一张“用户表”中。此时API设计如下:
GET /users/:id/friends - 获取用户:id的朋友列表
GET /users/:id/friends/:id2 - 获取用户:id朋友中ID号为:id2的朋友
POST /users/:id/friends - 用户:id新增一个朋友
PUT /users/:id/friends/:id2 - 更新朋友:id2的信息
PATCH /users/:id/friends/:id2 - 更新朋友:id2的部分信息
DELETE /users/:id/friends/:id2 - 删除用户:id的朋友:id2
问题1:API中的资源用单数形式还是复数形式?
建议:用复数表达。在最初的业务需求中,可能一个用户只有一个道具,因此获取用户道具资源时,似乎用GET /users/{id}/prop
就解决了,这种设计,会引导你在返回消息的body中只有一个道具信息,如:
:body {:prop-name "火球"}
当你的需求要扩展成一个用户拥有多个道具时,这个API就不适用了,此时,要想修改协议,那会变得很困难(整个客户端、协议、服务器端都要改动)。API设计成GET /users/{id}/props
将更合理,而返回消息的body则设计为:
:body [{:prop-name "火球"}]
可以发现,即使已有一个道具的场景,第二种设计也是完全满足的。并且在处理代码中,处理一个数组和单个元素其实不会耗费更多的工作。这样不仅保持了URL的格式的一致性,使接口更加精简,更提高了系统以及API的灵活性和可扩展性。
问题2:如果http的方法不足以表达我想要的操作时,怎么办?
建议:将其转换为资源的某一个属性,或作为一个子资源。
比如,要激活某一个道具,可以将道具的激活状态设计为道具资源的一个属性,然后通过PATCH /props/{id}
将道具的状态属性改为activited
;
再比如,github项目中,可以给自己喜欢的作者加“星”,此时,可以将“星”也设计为一种资源。加“星”时,用POST /users/{id}/stars
,即为该用户的星星集合中新建一颗星,取消“星”的时候,则可以用DELETE /users/{id}/stars/{star-id}
再比如,需要检索多个资源(可能是不同类的),API该怎么设计?网上有人推荐设计成GET /search
,我觉得这是不可行的,应为资源的定义应该用名词而非动词,因此个人建议是将这些不同类的资源再往上抽象一层进行命名,比如GET /objects
。
这是我们做Web Service时,最为常见的几个场景,该如何设计API呢?在网上有一条“最瘦URL”指导原则:
尽量保持瘦的资源URL,过滤条件、排序条件、搜索条件,可以使用url的查询参数的方式实现。
对于这一条指导原则,个人不是很赞同,感觉像是为了瘦而瘦,具体原因下面再解释。
如:获取所有职业为学生的男性用户,可设计为:
GET /users?sex=female&job=student
其语义为:我要获取的资源是用户的集合,然后我会根据性别和职业这两个条件进行筛选,最终职业为学生、性别为男性的用户集合将被过滤出来。
如:获取所有用户,以年龄降序,游戏等级升序输出。可设计为:
GET /users?sort=-age,level
其中,负号表示降序,不同排序条件以逗号隔开(可以为自定义符号,但注意不要用空格、’?’、’&’这些URL自身保留的符号)。
简单的搜索或者说服务内固定关键字的搜索用类似上面过滤的方式就可以解决。但如果服务使用了全文搜索引擎(如Solr、ElasticSearch),则可以在API中暴露一个接口供使用者使用,如:
GET /users?q=searchString&sex=female
其中searchString可以是满足Solr或ElasticSearch检索规则的字符串,也可以是自定义结构,再在服务器中转为Solr或ElasticSearch检索规则的格式。
问题1:所谓“最瘦URI原则”是什么?为什么说它是不对的?
按照网上资料的说法,如果你在纠结是将过滤条件放在URL查询字符串中合适,还是放在URI中合适的时候,你应当保持URI最短。如上面过滤示例的API貌似也可以设计为GET /users/female
,那按照最瘦URI原则,应该是GET /users?sex=female
更好。这貌似是正确的,但其实是没有什么道理的,很可能给大家带来错误的引导:为了瘦而瘦。
在我看来,这条所谓的指导原则,其实是根本没有理解REST资源定义本质而拍脑袋提出来的。在REST风格架构中,资源是用URL来定义的,URL查询参数不参与资源定义。也就是说,从URL可以看出,你的服务是怎样定义资源的;反过来,你最初用资源抽象你的服务时,资源是怎么定义的,你的URI就会被设置成什么样子,和URL的胖瘦没有任何关系——我个人觉得,这才是真正应该遵守的原则。具体例子,在下个问题中给出?
问题2:
GET /users/females
的语义与GET /users?sex=female
语义的区别?
按照问题1
中的解释,URL是用来定义资源的。
/users/females
最终指向(endpoint)的资源是females
,也就是说,你对服务中存在的资源进行定义时,所有女性用户被定义为一种资源,可能所有男性用户也被定义为另外一种资源。对于调用者而言,在调用之前所有用户就已经按照男女性别分成两个小集合,你只要直接取男集合或是女集合,无须甄别,直接命中。
/users?sex=female
最终指向(endpoint)的资源为users
,即一个没有将男女分开的一个混杂的用户集合。对于调用者而言,他在调用的时候可以选择加或者不加查询参数。如果不加参数,即GET /users
,那么服务器应该返回所有用户的集合给他,如果加了sex=female查询参数,那么服务器应该从这个大集合中,过滤出性别为female的用户集合给客户端,但此时的资源还是users,而不是females。
问题3:条件到底是放在URL中,还是放在查询参数中?
建议:根据问题1和问题2的分析,这个问题应该很容易回答了:主要取决于你对资源的定义方式。如果在你的业务场景中,抽象出的资源只有用户这一个大集合,那么应该将female这种过滤条件放在查询参数中;如果你抽象出更细粒度的资源集合,如females和males,那么就直接是放在URL中。对于这个问题,没有“更好或更不好”这么一说,只有“对与不对”,如果你要设计一个好的RESTful服务和API就必须仔细斟酌,不能妥协。对资源的合理划分和定义,是决定RESTful服务和API是否优秀的关键。
RESTful风格架构中客户端与服务器端的信息交互是粗粒度,主要因为使用了超媒体的方式。但是在现实中,我们往往不需要某个资源的所有属性,那样还会带来带宽的压力。因此,在REST服务设计和API设计时,客户端应该可以指定自己需要资源的哪些属性。有些资料说的是“限制返回结果”,个人觉得这个说法会限制或者误导REST服务端的对于资源的定义:服务器端在进行资源定义时,如果已经决定将资源暴露给外部,并且决定外部使用者可以只获取资源的任意属性,那么客户端要做的只是从中进行挑选,它无权告诉服务器端该如何定义资源。
可以设计如下API来指定返回结果:
GET /users/{id}?fields=name,age
语义为:我要获取{id}用户这个资源的状态表述,但我只需要其中的name,和age,不需要其他。
本博客主要介绍了我们实际RESTful API设计时最为常见的集中情况,只涉及到关于URL设计,没有包含HEADER,状态码之类的。在后面的博客中,将对这些进行进一步的介绍。