如何设计出一个优秀的API

7,945 阅读15分钟

写作不易,欢迎点赞+关注

前言

之前写过两篇关于RESTful API的文章:

REST是Roy Fielding博士在2000年的论文中首次被提出。他是当时参与了HTTP协议规范的制定,后来根据这种规范而提出来的一种架构风格。

Fielding博士所定义的REST,经过二十多年的发展,范围已经被扩充了不小,适用面也大了不小。但Fielding本人其实在2008年发表过一篇文章,其用比较隐含的方式对这种扩充REST意义表达了不满。

但时代在不断的发展,对一种定义的扩充是不可避免的,其实有一个评论就说的不错:

很多人进入一个误区,那就是全,说函数式编程,那就一丁点面向对象都不能用,说restful,那非restful的接口就不能写

一个优秀的API与REST是不能简单的划上一个等号。

所以对API的定义是要更具有一般性:符合RPC风格的JSON+HTTP接口的系统
那么本文重点就应该是如何把这种定义的API设计得更加优美!

而且本文所讲的API设计是针对对外公开的,或者公司内部公共的API,换句话说就是你不知道有哪些开发人员使用。

API的设计思想

在设计一个API时,首先需要清楚将怎样的数据经由API公开出去。一种非常简单的API设计方法就是开发一套数据访问机制,它能够直接操作服务所用的数据库及其数据表中的数据。

但如果仅仅是通过封装SQL语句来进行API设计的话,那么结果可想而知,这种API将会非常难用:

  • 使用人员不清楚数据之间存在怎么的关系。
  • 把存储结构都公开了,存在安全问题。 所以说API的设计要在更高级别的抽象层次上进行。

确定好功能后,接下来就是URI设计。

URI的基本设计

一个重要的设计原则就是:提供的功能要一目了然且容易理解记忆

虽说API是提供给应用程序去执行的,但编写程序并决定使用哪个API的是人,所以设计出便于人去理解和记忆的API是可以有效的降低开发人员错误使用的几率。

具体来说可以分为以下这几条规则:

  • 短小且便于输入
  • 容易读懂
  • 没有大小写混用
  • 方便修改
  • 规则统一

短小且便于输入

换句话说就是URI要设计的简单易记,冗长的URI往往会使得开发人员头疼。比如说

http://api.example.com/service/api/search

该URI包含了'api'、'search'等词语,虽然可以看出这是用于检索某种信息的URI。但是主机名和路径都包含了'api',所以显得重复了。另外还有'service'这种看起来没啥作用的词语,所以可以更为简短的写为:

http://api.example.com/search

在表示相同信息量的情况下,尽可能使用简短的表述方式,这样更易于理解和记忆,并能减少输入错误。

容易读懂

比如上面的URI,不用任何提示,一看就知道是用于检索的。举个反例:

http://api.example.com/s

s表示什么?或许是search,但这是不确定的。该URI设计得过于简短,导致了开发人员无法理解。所以不能随便使用简写,即使是这种很常见的简写,比如说prod指的是product,我相信一般人看到该单词也会感到困惑。

除此之外,URI的设计要使用API里常用的英语单词。因为这玩意是美国人发明的,使用英语来描述能使得API更容易理解,特别是互联网是面向全世界的。

http://api.example.com/users/1234
http://api.example.com/yonghu/1234

第一个URI使用英语单词“users”,第二个URI使用了中文拼音“yonghu”。这两个URI明显是英语单词比较容易理解,即使在中国,拼音也不是作为日常交流使用的。“yonghu”这个比较简单还好说,遇上复杂一点的拼音,国人也要愣上几秒才反应过来是拼音。

另外,同样是要用英语单词来描述URI,根据所用的单词是否符合API语境,开发人员理解起来也有所不同。比如说检索的单词有很多,在API中常用的是“search”而不是“find”。因为这两个单词在英语语境中是表示不同的含义的:

  • find是指【寻找某个特定物品】,一般是将所要找的东西作为宾语。
  • search则是【在某个地方中寻找】,习惯将需要寻找的场所作为宾语。 这种区别对于我们中国人来说,即使是经过九年义务教育也不是很容易区分。所以要尽可能地了解并使用API中常用的单词,这一点非常重要。怎么了解?只能是多观察别人设计的API,并且要多参考多对比,这样才能选出合适的单词。

第三点就是要注意单词的【复数】的拼写。一般来说API中用单词的复数形式来表示【资源的集合】,但有些词语的单复数形式差异比较大,这时就要注意。比如说老鼠:mouse,它的复数是mice。再者单词末尾的s看起来是单词的复数,其实意义相差甚远,比如说content表示内容,而contents则表示目录;custom表示习惯,而customs则表示海关,关税。

不能大小写混用

大小写混用也会造成API难以理解,容易让人搞错,所以一般都建议全部使用小写。比如说:

http://api.example.com/Users/1234
http://api.example.com/users/1234

对于上面的URI,处理方式可以有很多种:

  • 无论访问哪一个URI都返回相同的结果
  • 将有大写字母的URI重定向到只有小写字母的URI进行处理
  • 将有大写字母的URI视为错误,返回NOT FOUND
  • 无法识别有大写字母的URI 对于普通的Web页面而言,采用返回301并将页面进行重定向的方法是最为合适。因为第一种方法会导致搜索引擎降低网页排名,但API不会涉及搜索引擎的可搜索性问题,重定向不会导致页面排名下降。

遇到多个单词连接时应该怎么办?比如:

http://api.example.com/popularUsers

有人会觉得使用驼峰法表示就行,就像例子那样,但一般建议是使用连字符【-】连接多个单词,这不仅仅是喜好问题。首先在URI中,域名是允许使用连字符的,而且是禁止使用下划线和不区分大小写。其次呢,点字符是具有特殊含义。所以,为了用和主机名一致的规则来统一对URI命名,使用连字符连接多个单词最为合适。

但最好的方法还是尽量避免在URI中使用多个单词。上述的例子可以改成:

http://api.example.com/users/popular

也就是像路径那样划分,或者将一部分内容作为查询参数。这样也能避免连接多个单词,这往往也能让URI变的更加容易理解。

方便修改

方便修改就是英语所说的【Hackable】。方便修改的URI指的是非常容易将某个URI修改一下就变为另一个URI。比如说获取某个用户的信息:

http://api.example.com/users/1234

从api上就能看到该用户ID为1234,并且可以猜测只要修改这个ID,就能访问到别的用户的信息。

有些人会觉得,提供API文档并且做好详细的说明,即使URI设计的复杂或者难以理解都不会有啥事。但这种想法是错的,因为大多数开发人员往往不会仔细阅读文档。而且从API使用的角度来看,如果在开发过程中还需要不断的翻阅文档,那这样的API显然会增加开发负担。

如果可以从某个URI信息关联到其他URI,那么即使不那么频繁地查阅文档,也可以顺利地进行开发,与此同时还能减少因没有阅读文档而引起的BUG。

当然,也有不少观点认为URI没有必要是可以“Hackable”,因为从【HATEOAS】的角度来看,所有的URI在处理流程中都应由服务器以链接的形式提供给使用人员。

规则统一

规则统一是指URI所用的单词和URI的结构要统一。比如说:

// 获取好友信息
http://api.example.com/friends?id=100

// 发送信息
http://api.example.com/friend/100/message

获取好友信息的API里使用的是【friends】是复数形式,ID信息通过查询参数进行传递。而获取信息的API使用的【friend】、【message】是单数形式,ID信息则是通过URI路径进行指定。这种设计会显得杂乱无章,不仅在视觉上没有美感,还在实现时造成混乱。

改成规则统一的URI,就容易理解很多

// 获取好友信息
http://api.example.com/friends/100

// 发送信息
http://api.example.com/friends/100/messages

HTTP方法

正确使用HTTP方法不仅可以减少URI的数量,而且更加符合HTTP的语义。比如说:

http://api.example.com/orders

对于这样一个API:

  • 使用GET方法,表示获取所有订单
  • 使用POST方法,表示新增一个订单 如此,一个URI就可以表示两个API功能。

URI和HTTP方法可以理解为操作对象和操作方法之间的关系。如果把URI当作API的操作对象-也就是【资源】,那么HTTP方法就表示为对资源进行怎样的操作。

URI中R表示【Resource】,即“资源”的意思,用于描述某种具体的数据信息。在Web的范畴中,Web页面所包含的内容就是一种资源;在API范畴中,通过URI获取的数据信息也是一种资源。HTTP方法所表示的就是对该资源进行怎样的操作,所以通过用不同的方法访问一个URI不但可以进行获取,还能更新和删除等操作。

HTTP方法主要有:

方法说明
GET获取资源
POST新增资源
PUT更新已有资源
DELETE删除资源
PATCH更新部分资源
HEAD获取资源的元信息
CONNECT建立特殊的链接通道
OPTIONS列出可对资源实行的方法
TRACE追踪请求-响应的传输路径

GET

GET,表示【获取信息】,一般用于获取URI指定的资源。GET具有幂等的特性,所谓幂等(Idempotent)也就是即使反复获取,也不会对访问的数据产生副作用。当然API开发者完全可以把GET实现为有副作用,但这是一件非常可怕的事情。比如说“GET一下URI,订单立马产生,返回订单已成功的界面”,这是不可接受的。

另外可以对GET请求的数据做缓存。这个缓存可以是浏览器本身(避免发出HTTP请求),也可以是Nginx或者服务器端(返回304,减少带宽消耗),甚至404页面也可以被缓存。

可能有不少人认为,GET请求携带的数据只能附加在查询参数中,这其实是浏览器的行为。想一想在浏览器上怎么发出一个GET请求?其实就两个方法,要么是用户自己在浏览器的地址栏上输入URI,要么是点击a标签的href中的URI。只有这两种行为浏览器才能发出GET请求,所以GET请求上携带一些参数就只能依靠URI附带的查询参数了,但是HTTP本身并没有这个限制。

POST

POST方法的初衷是【发送附属于指定URI的新建资源信息】,而不是更新资源信息。资源的更新和删除操作有其他HTTP方法完成。

但是在HTTP4.0表单中的method属性,该属性只支持GET和POST这两种方法。渐渐地就把更新、删除在内的操作都使用POST方法来实现(值得一提的是,HTTP5在草案中加入了表单允许使用PUT和DELETE方法的规范,但最后还是删掉了)。

POST方法是存在副作用的,是不幂等的。这也意味着不能随意多次执行,因此也不能被缓存。有不少公司会使用POST代替GET,理由通常也是POST比GET安全。呃(⊙﹏⊙),确实是安全一点点,但也只是一点而已。

因为HTTP是明文传输的,每个HTTP请求和返回的每一个字节都会在网络上被人瞧的一清二楚,所以这点安全不足以放弃 GET方法,安全问题使用HTTPS才是正道。

PUT

PUT和POST方法相类似,都可用于对服务器端的信息进行更新,但二者URI的指定方法有所不同。POST方法发送的数据“附属”于指定的URI,附属表示从属于URI之下。比如说在文件系统中,把文件放入目录后,文件就成了目录的附属部分。所以,对文件目录或分类等表示数据集合的URI进行POST操作后,就会产生从属于原有集合的新数据。

在另一方面,PUT方法则指定需要更新资源的URI本身,并对其内容进行覆盖。如果URI资源已经存在,PUT操作就意味着对资源进行更新。虽然HTTP协议也定义了当指定的资源不存在时,可以通过PUT操作发送数据,生成新的资源,但Web API一般只用PUT方法来更新数据,而一般会使用POST来生成新的资源。

PATCH

PATCH和PUT方法相同,都用于更新指定的资源。但PATCH方法并不是更新资源的全部信息,而是只更新资源的一部分。PUT方法会使用发送的数据替换原有的资源信息,而PATCH方法只会更新原有资源中的部分信息。

PATCH方法发布的路程可谓是一波三折。它最早是在RPC2068中被定义,但由于没什么人使用,后来在RFC2616中被删掉了,但是在2010年3月发布的RFC6789中被再次提起并定义。

HEAD

HEAD方法与GET方法类似,也是请求从服务器获取资源,服务器处理的机制也类似,但服务器一般不会返回请求的实体数据,只会传回响应头,也就是资源的【元信息】。

HEAD方法可以看作是GET的一个“简化版”。因为它的响应头与GET完全相同,所以可以用在很多并不真正需要资源的场景中,避免传输body数据带来的带宽消耗。

比如说,想要检查一个文件是否存在,只要发HEAD请求即可,没有必要用GET把整个文件都取下来。

查询参数与路径

最后说说怎么选择查询参数或者路径。

从设计的角度来看,凡是可以附加在查询参数里的信息也都可以附加在URI的路径里。那在设计URI的时候,怎么选择把客户指定的参数放在查询参数还是路径里面?开发的时候可以在心里问问自己这两个问题:

  • 是不是表示资源唯一的信息?
  • 是不是可以省略? 第一个问题:是不是表示唯一资源所需的信息?
    这主要基于【URI表示资源】的基本思想,比如用户ID,可以通过用户ID去获取用户的信息,并且该信息是唯一的,所以将用户ID放在路径上会更加合适。
http://api.example.com/users/100

另一方面,很多API将“token”等信息用查询参数来指定。这是因为“token”用于用户认证,和资源无关,所以用查询参数更加合理。

第二个问题:是不是可以省略?
如果一些参数可以省略,比如说offset、limit等,省略了也不会出错,所以放在查询参数更为合适。

http://api.example.com/users?username=zhangwinwin

结尾

最后用以上的理论看看掘金的API,在评论区写下你认为设计的如何? image.png

本文参考:

  • 《Web API的设计与开发》
  • 《RESTful Web APIs》

最后

webpack好文推荐:

创作不易,烦请动动手指点一点赞。

楼主github, 如果喜欢请点一下star,对作者也是一种鼓励