你可以把客户端看作是前端,把服务器看作是后端。
客户端(前端)和服务器(后端)之间的通信通常不是超级直接的。因此,我们使用一个叫作“应用编程接口”(或 API)的接口,作为客户端和服务器之间的中介。
因为 API 在这种客户端-服务器通信中起着至关重要的作用,所以我们在设计 API 时应该始终考虑到最佳实践。这有助于维护它们的开发人员和那些使用它们的人,在履行职责时不会遇到问题。
在这篇文章中,我将带你了解创建 REST API 时需要遵循的一些最佳实践。这将帮助你创建最好的 API,并使你的 API 用户使用起来更容易。
REST 是 Representational State Transfer 的缩写。它是由 Roy Fielding 博士在 2000 年他的博士论文中提出一种软件架构风格,用于指导网络应用的设计和开发,使得 Web API(网络应用编程接口)更加简单、灵活、可扩展和易于理解。
任何遵循 REST 设计原则的 API 都被称为 RESTful API。
简单地说,REST API 是两台计算机通过 HTTP(超文本传输协议)进行通信的媒介,与客户端和服务器的通信方式相同。
当你设计一个 REST API 时,你不应该在端点路径中使用动词。端点应该使用名词,表示它们各自的作用。
这是因为 HTTP 方法,例如 GET、POST、PUT、PATCH 和 DELETE,已经以动词形式执行基本的 CRUD(创建、读取、更新、删除)操作。
GET、POST、PUT、PATCH 和 DELETE 是最常见的 HTTP 动词。还有其他非 HTTP 标准动词,如 COPY、PURGE、LINK、UNLINK 等等。
因此,举例来说,一个端点不应该是这样的:
https://mysite.com/getPosts or https://mysite.com/createPost
它应该是这样的:
https://mysite.com/posts
你可以把你的 API 的数据看成是来自用户的不同资源的集合。
如果你有一个像 https://mysite.com/post/123
这样的端点,用 DELETE 请求删除一个帖子,或用 PUT 或 PATCH 请求更新一个帖子,可能是可以的,但它没有告诉用户在这个集合中可能还有一些其他的帖子。这就是为什么你的集合应该使用复数的名词。
所以,不应该是 https://mysite.com/post/123
,而是 https://mysite.com/posts/123
。
很多时候,不同的端点可以相互联系,所以你应该对它们进行嵌套,这样更容易理解它们。
例如,对于一个多用户博客平台,不同的帖子可能是由不同的作者写的,所以在这种情况下,像 https://mysite.com/posts/author
这样的端点会成为一个有效的嵌套。
同样地,帖子可能有各自的评论,所以要检索评论,可以使用 https://mysite.com/posts/{postId}/comments
这样的端点。
你应该避免超过 3 层的嵌套,因为这可能使 API 不那么优雅,降低可读性。
使用 URL 指定你要用的资源。使用 HTTP 方法来指定怎么处理这个资源。使用五种 HTTP 方法 POST,GET,PUT/PATCH,DELETE 可以提供 CRUD 功能(创建,获取,更新,删除)。
除了 POST 其他请求都具备幂等性(多次请求的效果相同)。需要注意的是 POST 和 PUT 最大的区别就是幂等性,所以 PUT 也可以用于创建操作,只要在创建前就可以确定资源的 ID。
简而言之,你应该让 HTTP 动词来处理端点的工作。因此,GET 将检索资源,POST 将创建资源,PUT 将更新整个资源,DELETE 将删除资源,PATCH 更新资源的局部数据。
有时,API 的数据库可能非常大。如果发生这种情况,从这样的数据库中检索数据可能非常缓慢。
过滤、排序和分页都是可以在 REST API 的集合上执行的操作。这样只能检索、排序和排列必要的数据,并将其分页,以防服务器请求过载。
以下是一个已过滤的端点的示例:
https://mysite.com/posts?tags=javascript
此端点将检索具有 JavaScript 标签的任何帖子。
在过去,接受和响应 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 包也仍然适用于同一目的。
接口回包时我们应该将实际数据包装在 data 字段中。
比如查询某个帖子详情 GET https://mysite.com/posts/{id}
回包内容可以是:
{
"code": 0,
"msg": "ok",
"data": {
"post": {"id":1, "content":"xxx"}
}
}
再如分页拉取帖子详情。
{
"code": 0,
"msg": "ok",
"data": {
"total": 100,
"posts": [
{"id":1, "content":"xxx"},
{"id":2, "content":"xxx"},
{"id":3, "content":"xxx"}
]
}
}
有时API调用并不涉及资源(如计算,翻译或转换)。
GET /translate?from=de_DE&to=en_US&text=Hallo
GET /calculate?param1=23¶m2=432
在这种情况下,API响应不会返回任何资源。而是执行一个操作并将结果返回给客户端。因此,您应该在URL中使用动词而不是名词,来清楚的区分资源请求和非资源请求。
提供对特定资源的搜索很容易。只需使用相应的资源集合URL,并将搜索字符串附加到查询参数中即可。
GET /employees?query=Paul
如果要对所有资源提供全局搜索,则需要用其他方法。前文提到,对于非资源请求URL,使用动词而非名词。因此,您的搜索网址可能如下所示:
GET /search?query=Paul // 返回 employees, customers, suppliers 等等。
一个合法的 HTTP URL 组成格式如下:
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 可能如下所示:
https://api.example.com/users/john-doe
而使用下划线的 URL 则可能如下所示:
https://api.example.com/users/john_doe
虽然两者在技术上都是有效的 URL,但前者更符合 REST API 的最佳实践。
查询字符串是 URL 的组成部分。URL 规范规定查询字符串的不同参数使用与号(&)分隔,参数名与值使用等号(=)分隔。
当我们在 URL Query 中命名参数名称与值时,建议使用下划线。
如一个使用下划线的查询参数可能如下所示:
https://api.example.com/users?first_name=john&last_name=doe
而使用连字符的查询参数则可能如下所示:
https://api.example.com/users?first-name=john&last-name=doe
虽然在技术上两者都是有效的,但使用下划线的查询参数更符合 REST API 的最佳实践,并且更容易读写和阅读。
你应该在对你的 API 请求的响应中始终使用常规的 HTTP 状态代码。这将帮助你的用户知道发生了什么——请求是否成功,或者是否失败,或者其他情况。
下面的表格显示了不同的 HTTP 状态代码范围和它们的含义:
状态码 | 含义 |
---|---|
1XX | 信息性回应,如 102 表示该资源正在处理中 |
2XX | 成功,如 200 表示请求被正确处理 |
3XX | 重定向,如 301 表示永久移动 |
4XX | 客户端错误,如 400 表示错误的请求,404 表示未找到资源 |
5XX | 服务器端错误,如 500 表示内部服务器错误 |
除了提供恰当的HTTP状态代码外,还应该在HTTP响应正文中提供有用且详细的错误描述。 如下所示:
请求:
GET /mysite.com/posts?category=unknow&page=1&size=10
如果入参有误,应该准确告知调用方。
// 400 Bad Request
{
"code": 10000,
"msg":"Invalid category. Valid values are 'biz' or 'tech'"
}
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 时,您不需要强迫客户端迁移到新版本,如果他们不想迁移的话。
API 的使用者未必知道,URL 是怎么设计的。一个解决方法就是,在响应中给出相关链接,便于下一步操作。这样的话,用户只要记住一个 URL,就可以发现其他的 URL。这种方法叫做 HATEOAS。
HATEOAS 是 Hypermedia As The Engine Of Application State 的缩写,从字面上理解是 “超媒体即是应用状态引擎” 。其原则就是客户端与服务器的交互完全由超媒体动态提供,客户端无需事先了解如何与数据或服务器交互。相反的,在一些 RPC 服务或 Redis、MySQL 等软件,需要事先了解接口定义或特定的交互语法。
举例来说,GitHub 的 API 都在 api.github.com 这个域名。访问它,就可以得到其他 URL。
{
...
"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 一步步查找就可以了。
当你创建 REST API 时,你需要帮助用户(消费者)正确学习并了解如何使用它。最好的方法是为 API 提供良好的文档。
文档应包含:
你可以用于 API 文档的最常用工具是 Swagger。你也可以使用 Postman 来记录你的 API,这是软件开发中最常见的 API 测试工具。
SSL 指的是安全套接层。这对于 REST API 设计的安全性至关重要。这将保护你的 API,使其更不容易受到恶意攻击。
你还应考虑其他安全措施,包括:使服务器和客户端之间的通信保密,确保使用 API 的任何人不会获得他们请求的以外的数据。
SSL 证书不难加载到服务器上,而且大多数情况下在第一年是免费的。即使需要购买,它们也并不昂贵。
运行在 SSL 上的 REST API 的 URL 与不运行在 SSL 上的 URL 的明显区别是 HTTP 中的 “s”:https://mysite.com/posts
运行在 SSL 上,http://mysite.com/posts
不运行在 SSL 上。
一般来说 API 的外在形式无非就是增删改查(当然具体的业务逻辑肯定要复杂得多),而查询又分为详情和列表两种,在 REST 中这就相当于通用的模板。
例如针对文章(Article)设计 API,那么最基础的 URL 就是这几种:
将 id 放在 URL 中而不是 Query 的其中一个好处是可以表示资源之间的层级关系,例如文章下面会有评论(Comment)和点赞(Like),这两项资源必然会属于某一篇文章,所以它们的 URL 应该是下面这样的。
评论:
这里有一点比较特殊,永远使用可以指向资源的最短 URL,也就是说既然 /comments/{id} 可以指向一条评论了,就不要用 /articles/{id}/comments/{id} 特意指出所属文章了。
点赞:
REST 中不建议出现动词,所以可以将这种关系作为资源来映射。并且由于大部分的关系查询都与当前的登录用户有关,所以也可以直接在关系所属的资源中返回关系状态,如点赞状态就可以直接在获取文章详情时返回。
注意,点赞文章我选择了 PUT 而不是 POST,因为我觉得点赞这种行为应该是幂等的,多次操作的结果应该相同。
删除单个资源可以在 URL PATH 中指定资源 ID ,如删除文章评论。
DELETE /comments/{id}
如果需要同时删除多条文章评论,URL 该如何设计呢?
常见的方式有如下几种。
第一种,使用 DELETE 方法,用多个资源 ID 放进 URL Query 中。
DELETE /api/resource?ids=1,2,3...
第二种,使用 DELETE 方法,用逗号分隔将多个资源 ID 放进 URL PATH 中。
DELETE /api/resource/1,2,3...
由于浏览器对 URL 的长度存在限制,上面两种方式如果操作的资源过多无法实现。实际上批量删除操作本身是一个非常敏感的操作,一般会对批量删除资源的数量做严格限制,所以不会出现太长的 URL。
第三种,使用 DELETE 方法,将需要删除的资源的 ID 放到请求体里面。
DELETE /api/resource
{
"ids":[1,2,3...]
}
HTTP 协议标准并没有规定 DELETE 请求不能带 Body,但是 DELETE 请求体在语义上没有意义,一些网关、代理、防火墙在收到 DELETE 请求后,会把请求的 Body 直接剥离掉,所以不建议 DELETE 携带 Body。
第四种,改用 POST 方法,将需要删除资源的 ID 放到请求体。
POST /api/resource/batch/
{
"method": "delete",
"ids": [1, 2, 3]
}
使用 POST 语义上不符合实际的删除动作。
推荐使用第一种方式,使用 DELETE 方法,多个资源 ID 放进 URL Query 中。就像我们使用 GET 请求多个资源时,将筛选条件放到 Query 参数中。
GET /comments/{id} 获取单个评论
GET /comments?ids=1,2,3... 获取多个评论
DELETE /comments/{id} 删除单个评论
DELETE /comments?ids=1,2,3... 删除多个评论
在这篇文章中,你了解了在创建 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