设计更好的 Web API

Apr 04, 2018 API https://git.io/vxQ40

不知道你的身边是不是经常有人抱着这样的观点 —— “接口能调通就行了,反正用户看不到,别管是不是规范、是不是好看了”。事实上,API 设计本来就不是给用户看的,而是给开发人员看的。做好 API 设计并不需要耽误很多的时间,但是把 API 设计得足够规范,让开发者一眼就能看出来每一个模型每一个字段的意义和用法,一方面可以减少大量的写 API 文档的工作量,可以很大程度上减少对接的双方的沟通成本;另一方面好的 API 设计很容易做抽象,对接双方都能复用大量的代码。各个接口大致都是相同的,但是就是没法抽象,必须每个都复制粘贴然后要做一些修改,这种情况也会得到改善。

先来看一些 Web API 设计的反例,大部分是实际工作中遇到的:

  • 不同的项目使用同一个名字
  • 用 api1、api2、api3… 来作为不同项目的 API 域名前缀
  • 接口和字段驼峰命名和下划线命名混用
  • 用 2 和 4 来表示 to 和 for
  • 看上去是 RESTful,但是只有 POST 和 GET
  • 不用 PATCH,针对数据每个字段的修改都要对应一个新的接口
  • 请求数据的格式 form-data 和 JSON 混用
  • 几乎没有资源型接口,接口全都是动词 + model 名
  • 新增操作叫 addModel、createModel、upload、generateModel 等各种都有
  • 列表查询操作叫 listModel、queryModel、modelList、list、getAll、all 等各种都有
  • 单个查询操作叫 findModel、searchModel、get 各种都有
  • 删除操作叫 removeModel、delete 都有,但是就是不用 HTTP 的 DELETE 方法
  • 虽然在 API 路径里有版本号,但是永远都是 v1 不管 API 怎么变化都不会改这个版本号

以上说了这么多反面教材,接下来是一些我总结的一些好的 Web API 设计模式,主要是 HTTP RESTful 风格的 API。如果是用 GraphQL 或者一些云厂商的 SDK 比如 AWS Web SDK、Firebase Web SDK 等类似于 RPC 的模式,也有部分可以参考。

命名规范

  • 使用简单、不冲突、不重复、有一定规律或模式的一个单词或者两个单词作为项目的命名,这个命名会用在接口的域名前缀、namespace 等多个地方,也可以叫做“内部代号”或者 Code name。这个 链接 举了很多这样的例子。
  • 用准确的英文(而非拼音、数字等)名词来表示数据模型。淘宝前端团队的这篇 《从达标到卓越 — API 设计之道》 里有更详细的描述。
  • 作为数据模型的单词应该是能够准确描述这个数据模型的含义的,如果不能用准确的单词表示,开发团队应该有一份统一的单词表(glossary)来做约定。
  • 对于单个数据实例和多个数据的集合,是有单复数区别的。但是这种事情最好交给一个统一的单复数的库去处理,比如 Rails 里面默认就集成了单复数处理,另外比如 pluralize 之类的库也能做类似的事情。

命名风格

我推荐的风格是数据库字段、API 接口名、字段名等使用小写字母 + 下划线做分割的模式,也叫 snake_case,以下解释为什么:

  • 接口名和字段名都直接会在 URL 里访问到,URL 虽然是大小写敏感的,但是因为 URL 和域名、文件名等都有关系,而且早期浏览器对 URL 是大小写不敏感的,所以有个不成文的约定就是 URL 默认就是全小写,除某些特定情况外(比如短链接使用大小写混合来保证用较少的位数包含较多的信息)。
  • 不用连字符(或者叫减号)做分割,因为数据字段会映射到编程语言里,而大部分编程语言里的连字符是关键字,大部分情况下是减法操作符或者表示负数的符号。
  • 相比于驼峰命名,下划线分割便于分割和合并,大部分编程语言里都有原生的字符串分割和拼接,相比之下驼峰命名做这样的操作就要复杂一些了。
  • 驼峰命名在 Java 和 JavaScript 里很常见,但是在其他一些编程语言里并不是主流风格。

字段设计

字段可以分为数据实例字段和数据模型字段。数据实例字段场景的就是 ID、创建时间、修改时间等:

  • 用来做 ID 字段名就叫 id,而不能叫 pid、photo_id、photoId 之类的。因为 id 是用来确定数据实例唯一性的标志,不是跟具体某个模型有关系的字段。
  • 关键时间用 create_atupdate_at 表示创建时间和修改时间,为什么不用 create_time,因为 time 有歧义,除了“时间”还有“次数”的含义。

以及数据模型字段的设计风格:

  • 尽量用简单、单一的单词作为字段,而非隐晦而复杂的单词。
  • 业界通用的缩写(主要是 acronyms,即首字母缩写)比如 urlhttp 等可以用来作为字段名。其他的缩写(主要是 abbreviation,即单词缩写)比如 descauthcalfunc 等尽量用完整形式以降低产生歧义的可能性。
  • 外键或者关联字段的命名也要统一,比如如果是 SQL,外键就叫 user_id,而 NoSQL 如果是把 user 实例直接复制过来了,那么关联字段应该叫 user

路由设计

我的建议是参考 Rails 的路由设计。Rails 是 MVC Web 后端框架的开山鼻祖,很多其他语言的框架比如 Laravel、Django、Spring 等或多或少都有“借鉴” Rails 的成分。以下是我在 Rails 官方文档上关于资源型数据的路由的设计规范的基础上增加了部分内容的表格:

HTTP Verb Path Controller#Action Usage
前端&后端 GET /photos photos#index 展示照片列表
前端 GET /photos/new photos#new 展示“新建照片”表单
后端 POST /photos photos#create 提交新建表单
前端&后端 GET /photos/:id photos#show 展示某个 id 的照片
前端 GET /photos/:id/edit photos#edit 展示编辑某个 id 的照片的表单
后端 PATCH/PUT /photos/:id photos#update 提交编辑表单
后端 DELETE /photos/:id photos#destroy 提交删除某个 id 照片的操作

为什么要这样做:

  • 充分利用了 HTTP 的几个基本操作,且符合 RESTful 风格。
  • 创建和删除的操作叫 create/destroy 而不叫 new/delete,因为操作的名字在前后端都有可能以函数名的形式体现,这就避免了在一些编程语言里跟 new/delete 和关键字冲突。
  • 可以把 photos 替换成任何其他资源模型的名字,不管是前端后端,所有操作的代码都能直接复用或抽象出来。
  • 保持对单一资源操作路由的简单性,多个资源关联的操作也能按照同样的思路很自然的设计。
  • 通过对固定模式的约定,不需要再去写复杂的接口文档,这个约定本身就是文档了。尤其是前后端分离且是不同的人开发的时候这一点尤其有用。

多资源的关联路由和非资源型路由的设计我这里就不过多叙述了,可以去参考 Rails 文档的 Active Record AssociationsRails Routing from the Outside In: Non-Resourceful Routes.

总结

设计好的 Web API 并没有想象的那么难,需要设计者去参考自己所用的那个框架的设计规范,以及参考流行框架的设计思路。如果遇到现有规范里没有的情况而需要自己设计的时候,一方面是参考主流框架、主流公司的 API 的做法,另一方面是要把“抽象”和“易读”落实到设计中去。