REST API 最佳实践

在 Web 开发中,REST API 在确保客户端和服务器之间的顺利通信方面发挥了重要作用。

你可以把客户端看作是前端,把服务器看作是后端。

客户端(前端)和服务器(后端)之间的通信通常不是超级直接的。因此,我们使用一个叫作“应用编程接口”(或 API)的接口,作为客户端和服务器之间的中介。

因为 API 在这种客户端-服务器通信中起着至关重要的作用,所以我们在设计 API 时应该始终考虑到最佳实践。这有助于维护它们的开发人员和那些使用它们的人,在履行职责时不会遇到问题。

在这篇文章中,我将带你了解创建 REST API 时需要遵循的一些最佳实践。这将帮助你创建最好的 API,并使你的 API 用户使用起来更容易。

0.什么是 REST API?

REST 是 Representational State Transfer 的缩写。它是由 Roy Fielding 博士在 2000 年他的博士论文中提出一种软件架构风格,用于指导网络应用的设计和开发,使得 Web API(网络应用编程接口)更加简单、灵活、可扩展和易于理解。

任何遵循 REST 设计原则的 API 都被称为 RESTful API。

简单地说,REST API 是两台计算机通过 HTTP(超文本传输协议)进行通信的媒介,与客户端和服务器的通信方式相同。

1.REST API 设计建议

1.用名词表示资源

当你设计一个 REST API 时,你不应该在端点路径中使用动词。端点应该使用名词,表示它们各自的作用。

这是因为 HTTP 方法,例如 GET、POST、PUT、PATCH 和 DELETE,已经以动词形式执行基本的 CRUD(创建、读取、更新、删除)操作。

GET、POST、PUT、PATCH 和 DELETE 是最常见的 HTTP 动词。还有其他非 HTTP 标准动词,如 COPY、PURGE、LINK、UNLINK 等等。

因此,举例来说,一个端点不应该是这样的:

代码语言:javascript
复制
https://mysite.com/getPosts or https://mysite.com/createPost

它应该是这样的:

代码语言:javascript
复制
https://mysite.com/posts

2.用复数名词表示集合

你可以把你的 API 的数据看成是来自用户的不同资源的集合。

如果你有一个像 https://mysite.com/post/123 这样的端点,用 DELETE 请求删除一个帖子,或用 PUT 或 PATCH 请求更新一个帖子,可能是可以的,但它没有告诉用户在这个集合中可能还有一些其他的帖子。这就是为什么你的集合应该使用复数的名词。

所以,不应该是 https://mysite.com/post/123,而是 https://mysite.com/posts/123

3.在端点上使用嵌套显示关系

很多时候,不同的端点可以相互联系,所以你应该对它们进行嵌套,这样更容易理解它们。

例如,对于一个多用户博客平台,不同的帖子可能是由不同的作者写的,所以在这种情况下,像 https://mysite.com/posts/author 这样的端点会成为一个有效的嵌套。

同样地,帖子可能有各自的评论,所以要检索评论,可以使用 https://mysite.com/posts/{postId}/comments 这样的端点。

你应该避免超过 3 层的嵌套,因为这可能使 API 不那么优雅,降低可读性。

4.用 HTTP 方法操作资源

使用 URL 指定你要用的资源。使用 HTTP 方法来指定怎么处理这个资源。使用五种 HTTP 方法 POST,GET,PUT/PATCH,DELETE 可以提供 CRUD 功能(创建,获取,更新,删除)。

除了 POST 其他请求都具备幂等性(多次请求的效果相同)。需要注意的是 POST 和 PUT 最大的区别就是幂等性,所以 PUT 也可以用于创建操作,只要在创建前就可以确定资源的 ID。

  • 获取:使用 GET 方法获取资源。GET 请求从不改变资源的状态。无副作用。GET 是幂等的。GET 具有只读的含义。因此,你可以完美的使用缓存。
  • 创建:使用 POST 创建新的资源(非幂等)。
  • 更新:使用 PUT 更新整个资源,PATCH 将部分修改应用于资源。PUT 和 PATCH 都是幂等的。
  • 删除:使用 DELETE 删除现有资源(幂等)。

简而言之,你应该让 HTTP 动词来处理端点的工作。因此,GET 将检索资源,POST 将创建资源,PUT 将更新整个资源,DELETE 将删除资源,PATCH 更新资源的局部数据。

5.用过滤、排序和分页请求数据

有时,API 的数据库可能非常大。如果发生这种情况,从这样的数据库中检索数据可能非常缓慢。

过滤、排序和分页都是可以在 REST API 的集合上执行的操作。这样只能检索、排序和排列必要的数据,并将其分页,以防服务器请求过载。

以下是一个已过滤的端点的示例:

代码语言:javascript
复制
https://mysite.com/posts?tags=javascript

此端点将检索具有 JavaScript 标签的任何帖子。

6.用 JSON 作为发送和接收数据的格式

在过去,接受和响应 API 请求主要是通过 XML 甚至 HTML 完成的。但如今,JSON(JavaScript Object Notation)已经在很大程度上成为发送和接收 API 数据的事实格式。

这是因为,以 XML 为例,对数据进行解码和编码往往有点麻烦——所以 XML 不再受到框架的广泛支持。

例如,JavaScript 有一个内置的方法来通过 fetch API 解析 JSON 数据,因为 JSON 主要是为它而生成的。但是如果你使用任何其他编程语言,如 Python 或 PHP,它们现在也都有解析和操作 JSON 数据的方法。

例如,Python 提供json.load()json.dumps()来处理 JSON 数据。

为了确保客户端正确地解释 JSON 数据,你应该在发出请求时将响应头中的 Content-Type 类型设置为 application/json

另一方面,对于服务器端的框架,许多框架会自动设置 Content-Type。例如,Express 现在有 express.json() 中间件来实现这一目的。body-parser NPM 包也仍然适用于同一目的。

7.将实际数据包装在 data 字段中

接口回包时我们应该将实际数据包装在 data 字段中。

比如查询某个帖子详情 GET https://mysite.com/posts/{id}回包内容可以是:

代码语言:javascript
复制
{
  "code": 0,
  "msg": "ok",
  "data": {
    "post": {"id":1, "content":"xxx"}
  }
}

再如分页拉取帖子详情。

代码语言:javascript
复制
{
  "code": 0,
  "msg": "ok",
  "data": {
    "total": 100,
    "posts": [
    	{"id":1, "content":"xxx"},
    	{"id":2, "content":"xxx"},
    	{"id":3, "content":"xxx"}
    ]
  }
}

8.非资源请求用动词

有时API调用并不涉及资源(如计算,翻译或转换)。

代码语言:javascript
复制
GET /translate?from=de_DE&to=en_US&text=Hallo
GET /calculate?param1=23&param2=432

在这种情况下,API响应不会返回任何资源。而是执行一个操作并将结果返回给客户端。因此,您应该在URL中使用动词而不是名词,来清楚的区分资源请求和非资源请求。

9.考虑特定资源搜索和跨资源搜索

提供对特定资源的搜索很容易。只需使用相应的资源集合URL,并将搜索字符串附加到查询参数中即可。

代码语言:javascript
复制
GET /employees?query=Paul

如果要对所有资源提供全局搜索,则需要用其他方法。前文提到,对于非资源请求URL,使用动词而非名词。因此,您的搜索网址可能如下所示:

代码语言:javascript
复制
GET /search?query=Paul   // 返回 employees, customers, suppliers 等等。

10.URL PATH 使用连字符分隔单词

一个合法的 HTTP URL 组成格式如下:

代码语言:javascript
复制
http(s)://<host>:<port>/<path>?<query>#<frag>

PATH 部分,REST API 的标准最佳实践是使用连字符(hyphen),而不是下划线(underscore)或驼峰(camelcase)。这是来自 Mark Masse 的《REST API Design Rulebook》的建议。

此外,搜索引擎也更喜欢使用连字符来分隔单词,使用连字符分隔单词,它们让搜索引擎更准确地理解 URL 中的单词和短语,这样搜索引擎就可以索引单个单词,有助于 SEO,很容易检索到这个 URL,排名靠前。

许多著名公司都遵循该实践方式,如 Stack Overflow。

如一个使用连字符的 REST API URL 可能如下所示:

代码语言:javascript
复制
https://api.example.com/users/john-doe

而使用下划线的 URL 则可能如下所示:

代码语言:javascript
复制
https://api.example.com/users/john_doe

虽然两者在技术上都是有效的 URL,但前者更符合 REST API 的最佳实践。

11.URL Query 使用下划线分隔单词

查询字符串是 URL 的组成部分。URL 规范规定查询字符串的不同参数使用与号(&)分隔,参数名与值使用等号(=)分隔。

当我们在 URL Query 中命名参数名称与值时,建议使用下划线。

如一个使用下划线的查询参数可能如下所示:

代码语言:javascript
复制
https://api.example.com/users?first_name=john&last_name=doe

而使用连字符的查询参数则可能如下所示:

代码语言:javascript
复制
https://api.example.com/users?first-name=john&last-name=doe

虽然在技术上两者都是有效的,但使用下划线的查询参数更符合 REST API 的最佳实践,并且更容易读写和阅读。

12.使用 HTTP 状态码

你应该在对你的 API 请求的响应中始终使用常规的 HTTP 状态代码。这将帮助你的用户知道发生了什么——请求是否成功,或者是否失败,或者其他情况。

下面的表格显示了不同的 HTTP 状态代码范围和它们的含义:

状态码

含义

1XX

信息性回应,如 102 表示该资源正在处理中

2XX

成功,如 200 表示请求被正确处理

3XX

重定向,如 301 表示永久移动

4XX

客户端错误,如 400 表示错误的请求,404 表示未找到资源

5XX

服务器端错误,如 500 表示内部服务器错误

13.提供有用的错误消息

除了提供恰当的HTTP状态代码外,还应该在HTTP响应正文中提供有用且详细的错误描述。 如下所示:

请求:

代码语言:javascript
复制
GET /mysite.com/posts?category=unknow&page=1&size=10

如果入参有误,应该准确告知调用方。

代码语言:javascript
复制
// 400 Bad Request
{
	"code": 10000,
	"msg":"Invalid category. Valid values are 'biz' or 'tech'"
}

14.明确版本划分

REST API 应该有不同的版本,所以你不会强迫客户(用户)迁移到新版本。如果你不小心,这甚至可能破坏应用程序。

网络开发中最常见的版本控制系统之一是语义版本控制。

语义版本管理的一个例子是 1.0.0、2.1.2 和 3.3.4。第一个数字代表主要版本,第二个数字代表次要版本,第三个数字代表补丁版本。

许多科技巨头和个人的 RESTful API 通常是这样的:https://mysite.com/v1/ 代表版本 1,https://mysite.com/v2/ 代表版本 2。

Facebook 的 API 版本是这样的:

在这里插入图片描述

Spotify 以同样的方式做他们的版本管理:

在这里插入图片描述

并不是每个 API 都是这样的,Mailchimp 的 API 版本是这样的:

在这里插入图片描述

当您以这种方式提供 REST API 时,您不需要强迫客户端迁移到新版本,如果他们不想迁移的话。

15.使用 HATEOAS

API 的使用者未必知道,URL 是怎么设计的。一个解决方法就是,在响应中给出相关链接,便于下一步操作。这样的话,用户只要记住一个 URL,就可以发现其他的 URL。这种方法叫做 HATEOAS。

HATEOAS 是 Hypermedia As The Engine Of Application State 的缩写,从字面上理解是 “超媒体即是应用状态引擎” 。其原则就是客户端与服务器的交互完全由超媒体动态提供,客户端无需事先了解如何与数据或服务器交互。相反的,在一些 RPC 服务或 Redis、MySQL 等软件,需要事先了解接口定义或特定的交互语法。

举例来说,GitHub 的 API 都在 api.github.com 这个域名。访问它,就可以得到其他 URL。

代码语言:javascript
复制
{
  ...
  "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",
  ...
}

上面的回应中,挑一个 URL 访问,又可以得到别的 URL。对于用户来说,不需要记住 URL 设计,只要从 api.github.com 一步步查找就可以了。

16.提供准确的 API 文档

当你创建 REST API 时,你需要帮助用户(消费者)正确学习并了解如何使用它。最好的方法是为 API 提供良好的文档。

文档应包含:

  • API 的相关端点
  • 端点的示例请求
  • 在几种编程语言中的实现
  • 不同错误的消息列表及其状态代码

你可以用于 API 文档的最常用工具是 Swagger。你也可以使用 Postman 来记录你的 API,这是软件开发中最常见的 API 测试工具。

17.使用 SSL 保障安全

SSL 指的是安全套接层。这对于 REST API 设计的安全性至关重要。这将保护你的 API,使其更不容易受到恶意攻击。

你还应考虑其他安全措施,包括:使服务器和客户端之间的通信保密,确保使用 API 的任何人不会获得他们请求的以外的数据。

SSL 证书不难加载到服务器上,而且大多数情况下在第一年是免费的。即使需要购买,它们也并不昂贵。

运行在 SSL 上的 REST API 的 URL 与不运行在 SSL 上的 URL 的明显区别是 HTTP 中的 “s”:https://mysite.com/posts 运行在 SSL 上,http://mysite.com/posts 不运行在 SSL 上。

3.示例

一般来说 API 的外在形式无非就是增删改查(当然具体的业务逻辑肯定要复杂得多),而查询又分为详情和列表两种,在 REST 中这就相当于通用的模板。

例如针对文章(Article)设计 API,那么最基础的 URL 就是这几种:

  • GET /articles: 文章列表
  • GET /articles/{id}:文章详情
  • POST /articles: 创建文章
  • PUT /articles/{id}:修改文章
  • PATCH /articles/{id}:修改文章的部分信息
  • DELETE /articles/{id}:删除文章

将 id 放在 URL 中而不是 Query 的其中一个好处是可以表示资源之间的层级关系,例如文章下面会有评论(Comment)和点赞(Like),这两项资源必然会属于某一篇文章,所以它们的 URL 应该是下面这样的。

评论:

  • GET /comments/{id}: 获取单个评论
  • GET /articles/{id}/comments: 某篇文章的评论列表
  • POST /articles/{id}/comments: 在某篇文章中创建评论
  • PUT /comments/{id}: 修改评论
  • PATCH /comments/{id}: 修改评论的部分信息
  • DELETE /comments/{id}: 删除评论

这里有一点比较特殊,永远使用可以指向资源的最短 URL,也就是说既然 /comments/{id} 可以指向一条评论了,就不要用 /articles/{id}/comments/{id} 特意指出所属文章了。

点赞:

  • GET /articles/{id}/like:查看文章是否被点赞
  • PUT /articles/{id}/like:点赞文章
  • DELETE /articles/{id}/like:取消点赞

REST 中不建议出现动词,所以可以将这种关系作为资源来映射。并且由于大部分的关系查询都与当前的登录用户有关,所以也可以直接在关系所属的资源中返回关系状态,如点赞状态就可以直接在获取文章详情时返回。

注意,点赞文章我选择了 PUT 而不是 POST,因为我觉得点赞这种行为应该是幂等的,多次操作的结果应该相同。

4.FAQ

批量删除接口如何设计?

删除单个资源可以在 URL PATH 中指定资源 ID ,如删除文章评论。

代码语言:javascript
复制
DELETE  /comments/{id}

如果需要同时删除多条文章评论,URL 该如何设计呢?

常见的方式有如下几种。

第一种,使用 DELETE 方法,用多个资源 ID 放进 URL Query 中。

代码语言:javascript
复制
DELETE /api/resource?ids=1,2,3...

第二种,使用 DELETE 方法,用逗号分隔将多个资源 ID 放进 URL PATH 中。

代码语言:javascript
复制
DELETE /api/resource/1,2,3...

由于浏览器对 URL 的长度存在限制,上面两种方式如果操作的资源过多无法实现。实际上批量删除操作本身是一个非常敏感的操作,一般会对批量删除资源的数量做严格限制,所以不会出现太长的 URL。

第三种,使用 DELETE 方法,将需要删除的资源的 ID 放到请求体里面。

代码语言:javascript
复制
DELETE /api/resource
{
  "ids":[1,2,3...]
}

HTTP 协议标准并没有规定 DELETE 请求不能带 Body,但是 DELETE 请求体在语义上没有意义,一些网关、代理、防火墙在收到 DELETE 请求后,会把请求的 Body 直接剥离掉,所以不建议 DELETE 携带 Body。

第四种,改用 POST 方法,将需要删除资源的 ID 放到请求体。

代码语言:javascript
复制
POST /api/resource/batch/
{
	 "method": "delete",
	 "ids": [1, 2, 3]
}

使用 POST 语义上不符合实际的删除动作。

推荐使用第一种方式,使用 DELETE 方法,多个资源 ID 放进 URL Query 中。就像我们使用 GET 请求多个资源时,将筛选条件放到 Query 参数中。

代码语言:javascript
复制
GET /comments/{id} 获取单个评论
GET /comments?ids=1,2,3... 获取多个评论

DELETE /comments/{id} 删除单个评论
DELETE /comments?ids=1,2,3... 删除多个评论

5.小结

在这篇文章中,你了解了在创建 REST API 时需要记住的几个最佳实践。

将这些最佳实践和惯例付诸实践是很重要的,这样你就可以创建功能强大的应用程序,使其运行良好、安全,并最终使你的 API 用户能够更加容易地使用它。


参考文献

API Design Patterns and Best Practices | API Guide - Moesif
REST API Best Practices – REST Endpoint Design Examples - freeCodeCamp
REST接口设计规范 - 随遇而安
RESTful API 设计最佳实践 - 稀土掘金
Hyphen, underscore, or camelCase as word delimiter in URIs?
Dash (Hyphen) or Underscore in URLs: Which one to use and when? - Terminus Blog
Delete multiple records using REST - Stack Overflow