小皮博客 | Xiaopi's Blog

86-RestAPI设计指南-1-引言

类似于阿里巴巴的开发规范或者谷歌的开发规范,API设计是目前很多场景下的基本功,所以这里给出一个笔者的最佳实践(Best Practice).

大纲

  1. 引言
  2. 基本的设计原则
  3. 良好的工具和选型
  4. API的基础
  5. 路径和参数设计
  6. 选择与权衡
  7. 其他最佳实践
  8. 更多的约定
  9. 一个文档规范的示范
  10. 参考文献

引言

REST,即Representataional State Transfer的缩写。关于RESTful架构,可以参考《架构之美》
中的定义。

  • 客户端和服务器之间的交互在请求间是无状态的,每个请求都必须包含理解请求的全部信息。
  • 在此基础上,服务更容易实现分布式,水平扩展,异步处理和可重入(幂等请求)

REST架构中的基本定义

  • 资源(Resource): 网络上的所有内容都表述成一个资源,一个实体或者一个具体的信息。它可以是一段文本,一张图片,一首歌曲,或者一种服务。
  • 统一资源定位符(URI, Universal Resource Identifier): 一个资源的识别符或者说是一个地址,通过URI可以定位到一个唯一的资源,网络上每个资源都有一个唯一的识别符。
  • 状态转换(State Transfer): 所有资源都共享统一的接口,以便在客户端和服务器之间传输状态。客户端与服务器端互动的过程,通常涉及到服务器端数据和状态变化的过程,比如文件被修改,访问数量增加等。
  • 使用基本的HTTP请求方法
    • GET: 获取资源(幂等并且安全的)。
    • POST: 新建资源
    • PUT: 更新资源
    • DELETE: 删除动作
  • Hypermedia: 应用程序状态的引擎,实现hypermeida是API的最佳实践之一,可以参考api.github.com,后文也会再讲到。

基本的设计原则

协议(Protocol)

API与客户端通信的协议,应该尽量使用HTTPs。除此之外,白名单机制,VPN可以提高更多安全性。遵循“所有人应该知道他所需要完成工作必备的最少的知识”的原则。而一个接口,只有在访问者必须使用它时,才告诉这个访问者。

域名(Domain)

应该将API部署到专用域名之下。好处显而易见。

  • 更容易做动静分离。
  • 更容易做服务降级和限流。
  • 更容易做到高吞吐量。
  • 更容易做流量分发。
  • 更容易进行之后的水平扩展和服务拆分。

Sample: https://api.groupname.domain.io/

更好的实践是,将服务分组,并且根据情况进行必要的层次和分组(group)。这里的group可以包含事业部,也可以包含不同的客户,更可以包含不同的层次。

规范: https://api.[service-group].domain.io/

基本URL(Root URL)

基本的URL,在此文中指的是除了服务拆分之外的URL,这里的最佳实践是,可以包含环境,版本,分类,层次等,但是一个基本的URL,已经能决定除了接口或者服务粒度以外的所有事情。或者说,他可以决定,由一个作战单元(个人或者小的敏捷团队)日常维护的工作内容了。

Sample: https://api-dev.groupname.domain.io/mobile/v1/comment/[...]

  • 此处dev表示开发环境(这里的api默认表示生产,api-dev表示开发,职责仍然是唯一的)
  • groupname表示分组(可以包含更多层次信息或者更多分层,但是最佳实践是一般domain不超过两层,也可以默认约定.io域名后缀全部是为api部署服务)
  • mobile表示面向移动端
  • v1表示本类的接口版本
  • comment表示此本类服务为评论服务

环境划分原则(Env)

整体服务架构的划分原则不在此文档讨论范围之内,而一个最佳实践是,在domain当中就对环境做区分。

规范: https://api-[envname].groupname.domain.io/

接口的版本(API Version)

应该将API的版本号放入URL合适的层次。注意这里的Version即不表示客户端的版本,亦不表示服务器中服务的对应版本,而是特指该接口的版本。一般用来处理对接口做升级的情况。

规范: https://api-[env-name].groupname.domain.io/mobile/[version]/

  • v1和v2的区别,应该表示且仅表示接口的区别。
  • 不要发布无版本号的接口。
  • 使用简单的数字。
  • 服务分组加版本两个变量来共同决定接口的实现逻辑。

路径(EndPoints,端点)命名

此原则仅供参考。准备重写。
路径表示API的具体URL,每个URL唯一的表示一种资源。所以网址中不应该有动词,只应该有名词。而且所用的名词往往与代表的对象名称对应,一般来说某一种记录的集合,所以API名词当中应该使用复数。

  • URL: /cards/getCardById/{id} => HTTP GET: /cards/{id}
  • 如果某些动作是HTTP动词表达不了的,那么应该把动作当成资源去处理。
    • POST /accounts/1/transfer/500/to/2/ => POST /transaction?from=1&to=2&amount=500.00
  • 为了保持简单,只对所有资源使用复数。
    • /setting => /settings
    • /user => /users
  • 资源之间的层级关系应该表述清楚。
    • GET /hotels/1312/homes/ 返回酒店1312的所有房间。
    • GET /hotels/1312/homes/1209 返回酒店1312的1209房间。

HTTP动词(HTTP Verbs)

  • GET (SELECT): 从服务器取出对应的资源。
  • POST (CREATE): 新建一个资源。
  • PUT (UPDATE): 在服务器更新资源。(客户端提供改变后的完整的资源,所以应该少用)
  • PATCH (UPDATE): 在服务器更新资源。(客户端提供改变的属性)
  • DELETE (DELETE): 删除资源。
  • HEAD: 获取资源的元数据。
  • OPTIONS: 获取信息,关于哪些资源的哪些属性是客户端可以改变的。

最佳实践

  • 此处的最佳实践是,大家约定好我们的动作处理原则,并且在整个系统(组织或者公司)内保持统一。
  • GET方法不应该涉及状态改变。
  • 很多时候我们还需要收集客户端的信息,但是REST本身是无状态的,所以收集的时候,应该独立于REST API的设计原则,独立设计体系来收集类似于Client, cookie, ip和device等信息。

不符合CRUD的情况

一般有如下建议

  • 使用POST 重构行为的action
  • 增加控制参数(整体来约定)
  • 把动作转换成资源

过滤信息(Filtering)

API应该提供参数,过滤返回结果。因为服务器端某个资源数量可能很多。比如用户的订单数,全国的酒店数等。过滤的语义应该包括对数据集合的过滤,排序,选择,和分页等功能。

  • ?limit=10: 返回指定条目的数据。
  • ?offset=10: 指定返回记录的开始位置。
  • ?pageNumber=2&perSize=100: 指定第几页,以及每页的记录数量。
  • ?sortBy=time&order=desc: 排序顺序及属性。
  • ?type=1: 指定筛选条件。

最佳实践:

  • 实际上,我们在处理API业务时,不可能像数据库查询那么简洁容易。所以参数定义应该更加单一职责,更加严谨。
  • 总是可以在输入参数中设计一个客户端需要的attrList, 由使用者来指定需要的属性列表。
  • 参数的设计上应该允许冗余。
  • 可以使用HTTP的定制头: X-Total-Count表示资源总数。

状态码(Status Codes)

  • 这里一般实践是包含两层,一层是中间件(比如网关,nginx,tomcat)返回的HTTP请求本身的状态码。另外还包含服务本身返回的状态码设计。
  • 这里需要单独开辟一个章节来定义状态码,后文会给出一个最佳实践。
  • 一个好的可以坚持的原则是,服务本身返回的状态码的前缀,应该和网关那一层保持一致。这样可以有最好的层次关系。.
  • 比如40001表示请求参数错误,其中400表示INVALID REQUEST。01表示具体的错误为参数错误。

错误处理(Error Handling)

如果状态码不是正确的返回,就应该返回出错信息。尽量使用详细的错误信息。
一个好的实践是,出错信息应该包含:
userMessage: 显示给用户的。
internalMessage: 显示给程序员调试用的。
code: 编码
guideline: 参考解决指南。

返回结果(Response)

  • 按照RESTful架构“宽进严出”的原则,返回应该被严格定义。
  • 全部使用JSON返回结构。
  • 基本约定:
    • GET /collection: 返回资源列表
    • GET /collection/resource: 返回单个对象
    • POST /collection/resource: 返回新生成的对象。
    • PUT /collection/resource: 返回完整的更新后的资源对象。
    • PATCH /collection/resource: 返回完整的更新后的资源对象。
    • DELETE /collection/resource: 返回一个空文档。
  • 实际执行过程中远比这个复杂,但是我们仍然可以有一些基本原则。

使用HATEOAS构建Hypermedia APIs

超媒体API很可能是RESTful API设计的未来。它们实际上是一个非常惊人的概念,可以追溯到HTTP和HTML的工作原理。我们可以使用HATEOAS in Spring来构建Hypermedia APIs,而在此之前,约定更加重要。

一个好的Hypermedia范例是 api.github.com

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
"current_user_url": "https://api.github.com/user",
"current_user_authorizations_html_url": "https://github.com/settings/connections/applications{/client_id}",
"authorizations_url": "https://api.github.com/authorizations",
"code_search_url": "https://api.github.com/search/code?q={query}{&page,per_page,sort,order}",
"commit_search_url": "https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}",
"emails_url": "https://api.github.com/user/emails",
"emojis_url": "https://api.github.com/emojis",
"events_url": "https://api.github.com/events",
"feeds_url": "https://api.github.com/feeds",
"followers_url": "https://api.github.com/user/followers",
"following_url": "https://api.github.com/user/following{/target}",
"gists_url": "https://api.github.com/gists{/gist_id}",
"hub_url": "https://api.github.com/hub",
"issue_search_url": "https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}",
"issues_url": "https://api.github.com/issues",
"keys_url": "https://api.github.com/user/keys",
"notifications_url": "https://api.github.com/notifications",
"organization_repositories_url": "https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}",
"organization_url": "https://api.github.com/orgs/{org}",
"public_gists_url": "https://api.github.com/gists/public",
"rate_limit_url": "https://api.github.com/rate_limit",
"repository_url": "https://api.github.com/repos/{owner}/{repo}",
"repository_search_url": "https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}",
"current_user_repositories_url": "https://api.github.com/user/repos{?type,page,per_page,sort}",
"starred_url": "https://api.github.com/user/starred{/owner}{/repo}",
"starred_gists_url": "https://api.github.com/gists/starred",
"team_url": "https://api.github.com/teams",
"user_url": "https://api.github.com/users/{user}",
"user_organizations_url": "https://api.github.com/user/orgs",
"user_repositories_url": "https://api.github.com/users/{user}/repos{?type,page,per_page,sort}",
"user_search_url": "https://api.github.com/search/users?q={query}{&page,per_page,sort,order}"
}

标准请求定义

标准的请求定义当中有很多最佳实践,比如Content-Type等等。好的实践是,我们早早的用很小的代价把content-type, language这些加入设计当中,可以避免后续很多问题。后文会给出最佳实践。我们也需要看看,HTTP请求当中,到底包含了哪些东西。

认证(Authentication)

认证的时候取决于API的使用者和生产者之间的关系,以及需要保护的程度。目前此处的最佳实践是采用OAuth 2.0当中合适的模式来构建。

文档(Documentation)

使用Swagger API+JSON,进行文档管理和信息描述。定义一个标准的,语言无关的,供人和计算机理解服务的文档。类似于SOAP当中的WSDL。

  • 需要满足API自动生成同步的在线文档。
  • 可以用于API设计review。
  • 方便测试人员了解API定义。
  • 可以作为客户产品文档的一部分进行发布。
  • 可以通过API Swagger文档生成使用者和生产者的骨架代码。
  • 由于API的灵活性,此处一般不做严格要求。

一个文档规范示范

版权声明

本文标题:86-RestAPI设计指南-1-引言

文章作者:盛领

发布时间:2019年01月13日 - 10:35:21

原始链接:http://blog.xiaoyuyu.net/post/46676be6.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

如您有任何商业合作或者授权方面的协商,请给我留言:sunsetxiao@126.com

盛领 wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
坚持原创技术分享,您的支持将鼓励我继续创作!