<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ RESTful API - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ RESTful API - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Wed, 10 Jun 2026 20:41:06 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/tag/rest-api/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ REST API 最佳实践——REST 端点设计实例 ]]>
                </title>
                <description>
                    <![CDATA[ 在 Web 开发中，REST API 在确保客户端和服务器之间的顺利通信方面发挥了重要作用。 你可以把客户端看作是前端，把服务器看作是后端。 客户端（前端）和服务器（后端）之间的通信通常不是超级直接的。因此，我们使用一个叫作“应用编程接口”（或 API）的接口，作为客户端和服务器之间的中介。 因为 API 在这种客户端-服务器通信中起着至关重要的作用，所以我们在设计 API 时应该始终考虑到最佳实践。这有助于维护它们的开发人员和那些使用它们的人，在履行职责时不会遇到问题。 在这篇文章中，我将带你了解创建 REST API 时需要遵循的 9 个最佳实践。这将帮助你创建最好的 API，并使你的 API 用户使用起来更容易。 首先，什么是 REST API REST 是Representational State Transfer 的缩写。它是由 Roy Fielding 在 2000 年创造的一种软件架构风格，用于指导网络的架构设计。 任何遵循 REST 设计原则的 API（应用编程接口）都被称为 RESTful。 简单地说，REST API 是两台计算机通过 HTTP（超文 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/rest-api-best-practices-rest-endpoint-design-examples/</link>
                <guid isPermaLink="false">63e66333e673d23d288785f9</guid>
                
                    <category>
                        <![CDATA[ RESTful API ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chengjun.L ]]>
                </dc:creator>
                <pubDate>Tue, 04 Jul 2023 03:30:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/02/api.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/rest-api-best-practices-rest-endpoint-design-examples/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">https://www.freecodecamp.org/news/rest-api-best-practices-rest-endpoint-design-examples/</a>
      </p><p>在 Web 开发中，REST API 在确保客户端和服务器之间的顺利通信方面发挥了重要作用。</p><p>你可以把客户端看作是前端，把服务器看作是后端。</p><p>客户端（前端）和服务器（后端）之间的通信通常不是超级直接的。因此，我们使用一个叫作“应用编程接口”（或 API）的接口，作为客户端和服务器之间的中介。</p><p>因为 API 在这种客户端-服务器通信中起着至关重要的作用，所以我们在设计 API 时应该始终考虑到最佳实践。这有助于维护它们的开发人员和那些使用它们的人，在履行职责时不会遇到问题。</p><p>在这篇文章中，我将带你了解创建 REST API 时需要遵循的 9 个最佳实践。这将帮助你创建最好的 API，并使你的 API 用户使用起来更容易。</p><h2 id="-rest-api">首先，什么是 REST API</h2><p>REST 是Representational State Transfer 的缩写。它是由 Roy Fielding 在 2000 年创造的一种软件架构风格，用于指导网络的架构设计。</p><p>任何遵循 REST 设计原则的 API（应用编程接口）都被称为 RESTful。</p><p>简单地说，REST API 是两台计算机通过 HTTP（超文本传输协议）进行通信的媒介，与客户端和服务器的通信方式相同。</p><h2 id="rest-api-">REST API 设计最佳实践</h2><h3 id="-json-">使用 JSON 作为发送和接收数据的格式</h3><p>在过去，接受和响应 API 请求主要是通过 XML 甚至 HTML 完成的。但如今，JSON（JavaScript Object Notation）已经在很大程度上成为发送和接收 API 数据的事实格式。</p><p>这是因为，以 XML 为例，对数据进行解码和编码往往有点麻烦——所以 XML 不再受到框架的广泛支持。</p><p>例如，JavaScript 有一个内置的方法来通过 fetch API 解析 JSON 数据，因为 JSON 主要是为它而生成的。但是如果你使用任何其他编程语言，如 Python 或 PHP，它们现在也都有解析和操作 JSON 数据的方法。</p><p>例如，Python 提供 <code>json.load()</code> 和 <code>json.dumps()</code> 来处理 JSON 数据。</p><p>为了确保客户端正确地解释 JSON 数据，你应该在发出请求时将响应头中的 <code>Content-Type</code> 类型设置为 <code>application/json</code>。</p><p>另一方面，对于服务器端的框架，许多框架会自动设置 <code>Content-Type</code>。例如，Express 现在有 <code>express.json()</code> 中间件来实现这一目的。<code>body-parser</code> NPM 包也仍然适用于同一目的。</p><h3 id="-">在端点中使用名词而不是动词</h3><p>当你设计一个 REST API 时，你不应该在端点路径中使用动词。端点应该使用名词，表示它们各自的作用。</p><p>这是因为 HTTP 方法，例如 <code>GET</code>、<code>POST</code>、<code>PUT</code>、<code>PATCH</code> 和 <code>DELETE</code>，已经以动词形式执行基本的 CRUD（创建、读取、更新、删除）操作。</p><p><code>GET</code>、<code>POST</code>、<code>PUT</code>、<code>PATCH</code> 和 <code>DELETE</code> 是最常见的 HTTP 动词。还有其他一些动词，如 <code>COPY</code>、<code>PURGE</code>、<code>LINK</code>、<code>UNLINK</code> 等等。</p><p>因此，举例来说，一个端点不应该是这样的：</p><p><code>https://mysite.com/getPosts or https://mysite.com/createPost</code></p><p>它应该是这样的： <code>https://mysite.com/posts</code></p><p>简而言之，你应该让 HTTP 动词来处理端点的工作。因此，<code>GET</code> 将检索数据，<code>POST</code> 将创建数据，<code>PUT</code> 将更新数据，<code>DELETE</code> 将删除数据。</p><h3 id="--1">用复数名词命名集合</h3><p>你可以把你的 API 的数据看成是来自用户的不同资源的集合。</p><p>如果你有一个像 <code>https://mysite.com/post/123</code> 这样的端点，用 <code>DELETE</code> 请求删除一个帖子，或用 <code>PUT</code> 或 <code>PATCH</code> 请求更新一个帖子，可能是可以的，但它没有告诉用户在这个集合中可能还有一些其他的帖子。这就是为什么你的集合应该使用复数的名词。</p><p>所以，不应该是 <code>https://mysite.com/post/123</code>，而是 <code>https://mysite.com/posts/123</code>。</p><h3 id="--2">在错误处理中使用状态码</h3><p>你应该在对你的 API 请求的响应中始终使用常规的 HTTP 状态代码。这将帮助你的用户知道发生了什么——请求是否成功，或者是否失败，或者其他情况。</p><p>下面的表格显示了不同的 HTTP 状态代码范围和它们的含义：</p><!--kg-card-begin: markdown--><table>
<thead>
<tr>
<th>状态码范围</th>
<th>含义</th>
</tr>
</thead>
<tbody>
<tr>
<td>100 – 199</td>
<td>信息性回应：例如，102 表示该资源正在处理中</td>
</tr>
<tr>
<td>300 – 399</td>
<td>重定向：例如，301 表示永久移动</td>
</tr>
<tr>
<td>400 – 499</td>
<td>客户端错误：400 表示错误的请求，404 表示未找到资源</td>
</tr>
<tr>
<td>500 – 599</td>
<td>服务器端错误：例如，500 表示内部服务器错误</td>
</tr>
</tbody>
</table>
<!--kg-card-end: markdown--><h3 id="--3">在端点上使用嵌套来显示关系</h3><p>很多时候，不同的端点可以相互联系，所以你应该对它们进行嵌套，这样更容易理解它们。</p><p>例如，对于一个多用户博客平台，不同的帖子可能是由不同的作者写的，所以在这种情况下，像 <code>https://mysite.com/posts/author</code> 这样的端点会成为一个有效的嵌套。</p><p>同样地，帖子可能有各自的评论，所以要检索评论，可以使用 <code>https://mysite.com/posts/postId/comments</code> 这样的端点。</p><p>你应该避免超过 3 层的嵌套，因为这可能使 API 不那么优雅和具有可读性。</p><h3 id="--4">使用过滤、排序和分页来检索请求的数据</h3><p>有时，API 的数据库可能非常大。如果发生这种情况，从这样的数据库中检索数据可能非常缓慢。</p><p>过滤、排序和分页都是可以在 REST API 的集合上执行的操作。这样只能检索、排序和排列必要的数据，并将其分页，以防服务器请求过载。</p><p>以下是一个已过滤的端点的示例：<code>https://mysite.com/posts?tags=javascript</code>。此端点将检索具有 JavaScript 标签的任何帖子。</p><h3 id="-ssl-">使用 SSL 保障安全</h3><p>SSL 指的是安全套接层。这对于 REST API 设计的安全性至关重要。这将保护你的 API，使其更不容易受到恶意攻击。</p><p>你还应考虑其他安全措施，包括：使服务器和客户端之间的通信保密，确保使用 API 的任何人不会获得他们请求的以外的数据。</p><p>SSL 证书不难加载到服务器上，而且大多数情况下在第一年是免费的。即使需要购买，它们也并不昂贵。</p><p>运行在 SSL 上的 REST API 的 URL 与不运行在 SSL 上的 URL 的明显区别是 HTTP 中的 “s”：<code>https://mysite.com/posts</code> 运行在 SSL 上，<code>http://mysite.com/posts</code> 不运行在 SSL 上。</p><h3 id="--5">明确版本划分</h3><p>REST API 应该有不同的版本，所以你不会强迫客户（用户）迁移到新版本。如果你不小心，这甚至可能破坏应用程序。</p><p>网络开发中最常见的版本控制系统之一是语义版本控制。</p><p>语义版本管理的一个例子是 1.0.0、2.1.2 和 3.3.4。第一个数字代表主要版本，第二个数字代表次要版本，第三个数字代表补丁版本。</p><p>许多科技巨头和个人的 RESTful API 通常是这样的：<code>https://mysite.com/v1/</code> 代表版本 1，<a href="https://mysite.com/v2">https://mysite.com/v2</a> 代表版本 2。</p><p>Facebook 的 API 版本是这样的：</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2021/09/facebook-versioning.jpg" class="kg-image" alt="facebook-versioning" width="600" height="400" loading="lazy"></figure><p>Spotify 以同样的方式做他们的版本管理：</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2021/09/spotify-versioning.jpg" class="kg-image" alt="spotify-versioning" width="600" height="400" loading="lazy"></figure><p>并不是每个 API 都是这样的情况。Mailchimp 的 API 版本是这样的：</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2021/09/mailchimp-ersioning.jpg" class="kg-image" alt="mailchimp-ersioning" width="600" height="400" loading="lazy"></figure><p>当你以这种方式创建 REST API 时，你不强制客户端在选择不迁移的情况下迁移到新版本。</p><h3 id="-api-">提供准确的 API 文档</h3><p>当你创建 REST API 时，你需要帮助用户（消费者）正确学习并了解如何使用它。最好的方法是为 API 提供良好的文档。</p><p>文档应包含：</p><ul><li>API 的相关端点</li><li>端点的示例请求</li><li>在几种编程语言中的实现</li><li>不同错误的消息列表及其状态代码</li></ul><p>你可以用于 API 文档的最常用工具是 Swagger。你也可以使用 Postman 来记录你的 API，这是软件开发中最常见的 API 测试工具。</p><h2 id="--6"><strong>总结</strong></h2><p>在这篇文章中，你了解了在创建 REST API 时需要记住的几个最佳实践。</p><p>将这些最佳实践和惯例付诸实践是很重要的，这样你就可以创建功能强大的应用程序，使其运行良好、安全，并最终使你的 API 用户能够更加容易地使用它。</p><p>谢谢你阅读本文。现在，去用这些最佳实践创建一些 API 吧。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ REST API 设计最佳实践手册——如何使用 JavaScript、Node.js 和 Express.js 构建 REST API ]]>
                </title>
                <description>
                    <![CDATA[ 原文：REST API Design Best Practices – How to Build a REST API with JavaScript, Node.js, and Express.js [https://www.freecodecamp.org/news/rest-api-design-best-practices-build-a-rest-api/] ，作者：Jean-Marc Möckel [https://www.freecodecamp.org/news/author/jeanmarcmoeckel/] 在过去几年我创建和使用过不少 API，期间我遇到过优秀的实践方式，也遭遇过极其不好的实践方式，但曙光总是存在。 网上有许多最佳实践相关的文章，但是它们大多数都缺乏实用性。通过少量示例来了解理论是一个好办法，但是我一直都在思考如何通过更实际的例子来展现 API 的应用。 简单的例子确实可以帮助概念的理解，也省去了复杂度。但实际情况往往并不简单，我确信你对此也深有体会。😁 这就是我决定写这个教程的原因。我将过去好的坏的学习经验都融入了到这边文章之中，并提供 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/rest-api-design-best-practices-build-a-rest-api/</link>
                <guid isPermaLink="false">628d91bd60237306d26070c7</guid>
                
                    <category>
                        <![CDATA[ RESTful API ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Wed, 25 May 2022 04:54:41 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/05/rest-api-design-course-header.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/rest-api-design-best-practices-build-a-rest-api/">REST API Design Best Practices – How to Build a REST API with JavaScript, Node.js, and Express.js</a>，作者：<a href="https://www.freecodecamp.org/news/author/jeanmarcmoeckel/">Jean-Marc Möckel</a></p><!--kg-card-begin: markdown--><p>在过去几年我创建和使用过不少 API，期间我遇到过优秀的实践方式，也遭遇过极其不好的实践方式，但曙光总是存在。</p>
<p>网上有许多最佳实践相关的文章，但是它们大多数都缺乏实用性。通过少量示例来了解理论是一个好办法，但是我一直都在思考如何通过更实际的例子来展现 API 的应用。</p>
<p>简单的例子确实可以帮助概念的理解，也省去了复杂度。但实际情况往往并不简单，我确信你对此也深有体会。😁</p>
<p>这就是我决定写这个教程的原因。我将过去好的坏的学习经验都融入了到这边文章之中，并提供例子，使文章易读易懂。我们会通过一个又一个最佳实践来创建一个完整的 API。</p>
<p>开始之前的注意事项：</p>
<p>如你所想，最佳实践并不是必须遵从的具体规则。它们是人们逐渐总结出来的有效的惯例，确实有一些成为现在的标准，但这并不意味着你需要百分之一百的采用这些实践。</p>
<p>最佳实践指导如何使 API 更加符合用户（使用 API 的人和其他工程师）的使用习惯、更加安全和提高性能。</p>
<p>请记住不同的项目有不同的实践方法，肯定存在一些情况下你无法遵守这些规范，每一个工程师都应该自己决定使用什么方法。</p>
<p>话不多说，让我们开始吧！</p>
<h2 id="">目录</h2>
<ul>
<li><a href="#our-example-project">示例项目</a>
<ul>
<li><a href="#prerequisites">前提条件</a></li>
<li><a href="#architecture">结构</a></li>
<li><a href="#basic-setup">基础设置</a></li>
</ul>
</li>
<li><a href="#rest-api-best-practices">REST API 最佳实践</a>
<ul>
<li><a href="#versioning">版本</a></li>
<li><a href="#name-resources-in-plural">用复数形式命名资源</a></li>
<li><a href="#accept-and-respond-with-data-in-json-format">以 JSON 格式接受和响应数据</a></li>
<li><a href="#respond-with-standard-http-error-codes">响应标准 HTTP 错误代码</a></li>
<li><a href="#avoid-verbs-in-endpoint-names">避免在端点使用动词</a></li>
<li><a href="#group-associated-resources-together-logical-nesting-">帮相关资源放在一起</a></li>
<li><a href="#integrate-filtering-sorting-pagination">集成过滤排序和分页功能</a></li>
<li><a href="#use-data-caching-for-performance-improvements">使用数据缓存提升性能</a></li>
<li><a href="#good-security-practices">好的安全实践</a></li>
<li><a href="#document-your-api-properly">编写 API 合适的文档</a></li>
</ul>
</li>
<li><a href="#conclusion">总结</a></li>
</ul>
<h2 id="our-example-project">示例项目</h2>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/alvaro-reyes-qWwpHwip31M-unsplash--1-.jpg" alt="alvaro-reyes-qWwpHwip31M-unsplash--1-" width="600" height="400" loading="lazy"></p>
<p>图片作者<a href="https://unsplash.com/@alvarordesign?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Alvaro Reyes</a>，来自<a href="https://unsplash.com/s/photos/project?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p>
<p>在正式开始在示例中应用最佳实践前，我先简单介绍一下我们要创建什么。</p>
<p>我们将为交叉训练应用（CrossFit Training Application）创建 REST API。交叉训练是一种健身方式，融合了竞技类运动和高强度训练，以及各种各样的运动元素（奥林匹克举重、体操等）。</p>
<p>这个应用可以创建、读取、更新和删除<strong>WOD</strong>（<strong>W</strong>orkout <strong>o</strong>f the <strong>D</strong>ay，即每日训练），帮助用户（健身馆主）指定和维护已有的健身计划。除此之外，还可以在一些重要的训练旁批注一些训练建议。</p>
<p>我们的工作就是设计和实现这个应用的 API。</p>
<h3 id="prerequisites">前提条件</h3>
<p>在学习这门教程之前，你必须使用过 JavaScript、Node.js、Express.js 以及后端架构，熟悉 REST 和 API 这类术语，并且了解<a href="https://en.wikipedia.org/wiki/Client%E2%80%93server_model">主从式架构（客户端/服务器架构）</a>。</p>
<p>当然你不需要成为这些话题的专家，熟悉并且有这些实际操作经验就足够了。</p>
<p>即便你不符合上述条件，也不是跳过这篇教程的理由。你还是可以从这篇文章中学到很多东西，具备上述条件可以帮助你更轻松地阅读这篇文章。</p>
<p>虽然这里的 API 是用 JavaScript 和 Express 写的，但不表示这些最佳实践仅适用于此。你也可以在其他的编程语言和框架中应用这些最佳实践。</p>
<h3 id="architecture">架构</h3>
<p>如前所述，我会是用 Express.js 来搭建 API。我不想弄得太复杂，所以我会使用 <strong>3 层结构</strong>：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/Bildschirmfoto-2022-04-25-um-14.33.24-1.png" alt="Bildschirmfoto-2022-04-25-um-14.33.24-1" width="600" height="400" loading="lazy"></p>
<p>在<strong>控制器</strong>，我们将处理所有 HTTP 相关的内容，也就是说我们在这里处理端点的请求和响应。在这层之上是 Express 的<strong>路由</strong>把请求传递给相应的控制器。</p>
<p>所有业务逻辑都在<strong>服务层</strong>，服务层导出特定服务（方法）供控制层器用。</p>
<p>第三层是<strong>数据访问层</strong>，在这里处理数据库。我们将导出一些处理数据的方法，如创建 WOD，供服务层使用。</p>
<p>在我们的教学示例中，我们不会使用 <em>真实的</em> 数据库，如 MongoDB 或者 PostgreSQL，因为我想专注于最佳实践本身。因此我们会使用本地 JSON 文件来模拟数据库，但是使用逻辑可以迁移到其他的数据库。</p>
<h3 id="basic-setup">基础设置</h3>
<p>现在我们开始配置 API 的基础设置。不会太复杂，我们只创建一个简单、有组织的结构。</p>
<p>首先，我们创建一个总文件目录结构，包含所有必需的文件和依赖项。创建完了之后，我们将快速地检查一下一切是否运行正常。</p>
<pre><code class="language-bash"># 创建项目目录并且打开这个目录
mkdir crossfit-wod-api &amp;&amp; cd crossfit-wod-api
</code></pre>
<pre><code class="language-bash"># 创建 src 目录并打开这个目录
mkdir src &amp;&amp; cd src
</code></pre>
<pre><code class="language-bash"># 创建子目录
mkdir controllers &amp;&amp; mkdir services &amp;&amp; mkdir database &amp;&amp; mkdir routes
</code></pre>
<pre><code class="language-bash"># 创建 index 文件（API 接入点）
touch index.js
</code></pre>
<pre><code class="language-bash"># 我们现在位于 src 目录，所以要返回一级
cd .. 

# 创建 package.json 文件
npm init -y
</code></pre>
<p>安装基础设置的所有依赖项：</p>
<pre><code class="language-bash"># 开发依赖项
npm i -D nodemon 

# 依赖项 
npm i express
</code></pre>
<p>在你最喜欢使用的文字处理器中打开我们的项目，然后配置 Express：</p>
<pre><code class="language-javascript">// 在 src/index.js 中
const express = require("express"); 

const app = express(); 
const PORT = process.env.PORT || 3000; 

// 供测试用代码
app.get("/", (req, res) =&gt; { 
    res.send("&lt;h2&gt;It's Working!&lt;/h2&gt;"); 
}); 

app.listen(PORT, () =&gt; { 
    console.log(`API is listening on port ${PORT}`); 
});
</code></pre>
<p>在package.json中添加 <strong>"dev"</strong> 脚本：</p>
<pre><code class="language-json">{
  "name": "crossfit-wod-api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "nodemon src/index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "nodemon": "^2.0.15"
  },
  "dependencies": {
    "express": "^4.17.3"
  }
}
</code></pre>
<p>nodemon 可以确保每次你保存更改的时候，重新启动开发服务器。</p>
<p>启动开发服务器：</p>
<pre><code class="language-bash">npm run dev
</code></pre>
<p>查看控制台，会收到消息 <strong>"API is listening on port 3000"</strong>。</p>
<p>在浏览器中打开 <strong>localhost:3000</strong>。如果一切设置正确，你会看到下面内容：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/Bildschirmfoto-2022-04-30-um-11.09.44.png" alt="Bildschirmfoto-2022-04-30-um-11.09.44" width="600" height="400" loading="lazy"></p>
<p>太好了！我们已经设置好应用最佳实践的环境。</p>
<h2 id="rest-api-best-practices">REST API 最佳实践</h2>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/constantin-wenning-idDvA4jPBO8-unsplash--1-.jpg" alt="constantin-wenning-idDvA4jPBO8-unsplash--1-" width="600" height="400" loading="lazy"></p>
<p>图片作者<a href="https://unsplash.com/@conniwenningsimages?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Constantin Wenning</a>，来自<a href="https://unsplash.com/s/photos/handshake?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p>
<p>很好！我们已经完成了 Express 的基础设置，现在我们可以根据最佳实践来扩展 API 了。</p>
<p>我们从最简单的基础 CRUD 端点开始，之后我们将使用最佳实践来扩展 API。</p>
<h3 id="versioning">版本</h3>
<p>稍等一下，在我们编写具体的 API 代码之前，我们要关注一下版本。和其他所有应用一样，我们的 API 也需要迭代、更新功能，所以给我们的 API 制定版本十分重要。</p>
<p>这样做最大的优势是当我们在创建新功能的时候并不影响客户端继续运行旧版本。</p>
<p>我们并不强迫用户直接使用我们的新版本，用户可以继续使用旧的版本，直到新版本稳定后再迁移到新版本。</p>
<p>当下版本和新版本并行运行，互不干扰。</p>
<p>那我们如何区分不同的版本呢？一种不错的做法是在 URL 添加 v1、v2 这样的路径段。</p>
<pre><code class="language-javascript">// 版本1 
"/api/v1/workouts" 

// 版本2 
"/api/v2/workouts" 

// ...
</code></pre>
<p>这就是我们暴露给外部，其他开发者也可以使用的部分。同时，我们也需要调整项目结构来区分不同的版本。</p>
<p>管理 Express API 版本的方法各式各样。本教程中我将在 <strong>src</strong> 目录下创建一个版本目录，如 <strong>v1</strong>：</p>
<pre><code class="language-bash">mkdir src/v1
</code></pre>
<p>现在我们将路由目录移动到新的 v1 目录下：</p>
<pre><code class="language-bash"># 获取当前路径（复制）
pwd 

# 将 “routes” 添加到 “v1” （使用 {pwd} 插入新的路径）
mv {pwd}/src/routes {pwd}/src/v1
</code></pre>
<p>新目录 <strong>/src/v1/routes</strong> 将存储版本 1 的所有路由。之后我们会在里面添加“真实”的内容，但现在我们简单添加一个 <strong>index.js</strong> 文件来简单测试一下。</p>
<pre><code class="language-bash"># 在 /src/v1/routes 中
touch index.js
</code></pre>
<p>我们开启一个简单的路由：</p>
<pre><code class="language-javascript">// 在 src/v1/routes/index.js 中
const express = require("express");
const router = express.Router();

router.route("/").get((req, res) =&gt; {
  res.send(`&lt;h2&gt;Hello from ${req.baseUrl}&lt;/h2&gt;`);
});

module.exports = router;
</code></pre>
<p>现在我们将在 v1 内部的根入口点 src/index.js 接上路由：</p>
<pre><code class="language-javascript">// 在src/index.js
const express = require("express");
// *** 添加 ***
const v1Router = require("./v1/routes");

const app = express();
const PORT = process.env.PORT || 3000;

// *** 删除 ***
app.get("/", (req, res) =&gt; {
  res.send("&lt;h2&gt;It's Working!&lt;/h2&gt;");
});

// *** 添加 ***
app.use("/api/v1", v1Router);

app.listen(PORT, () =&gt; {
  console.log(`API is listening on port ${PORT}`);
});
</code></pre>
<p>再登陆浏览器浏览 <strong>localhost:3000/api/v1</strong>，你会看到以下画面：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/Bildschirmfoto-2022-04-30-um-11.22.28.png" alt="Bildschirmfoto-2022-04-30-um-11.22.28" width="600" height="400" loading="lazy"></p>
<p>祝贺你！你已经调整好了项目结构以适应不同版本。现在我们通过版本 1 的路由来传入请求，之后每一个请求会连接相应的控制器方式。</p>
<p>再继续下一步之前，我想强调一些内容。</p>
<p>我们把路由目录迁移到了 v1 目录下，其他目录如控制器和服务器仍在 src 目录下。因为我们搭建的 API 比较小，所以这么做没有问题，每一个版本我们使用相同的控制器和服务器。</p>
<p>当 API 逐渐壮大，比方说 2 版本需要使用不同的控制方法的话，最好还是把控制器目录放在 v2 目录下，这样就打包了这个版本所有的特定逻辑。</p>
<p>另一个这样做的原因是，我们可能在其他版本中想要改变某个服务器，但我们并不想要中断除此之外的版本。所以推荐把服务器目录也迁移到特定版本目录。</p>
<p>在我们的例子当中仅区分路由的版本是可行的。尽管如此。切记当 API 壮大需要改变的时候，拥有一个清晰的目录结构十分重要。</p>
<h3 id="name-resources-in-plural">用复数形式命名资源</h3>
<p>设置完结构后我们就进入了真正的 API 搭建了。我希望从基础的 CRUD 端点开始。</p>
<p>也就是说，我们从实现创建、读取、更新和删除交叉训练端点开始。</p>
<p>首先，让我们为训练连接控制器、服务器和路由。</p>
<pre><code class="language-bash">touch src/controllers/workoutController.js 

touch src/services/workoutService.js 

touch src/v1/routes/workoutRoutes.js
</code></pre>
<p>我通常喜欢从编写路由开始。让我们思考一下如何给端点命名。这里就会运用到最佳实践。</p>
<p>我们可以将端点命名为 <strong>/api/v1/workout</strong>，因为我们只添加一个交叉训练，对不对？虽说这样做基本上没什么问题，但是这样可能会造成误解。</p>
<p>谨记：你的 API 会被其他的<strong>人类</strong>使用，所以必须精准。这一规则也适用于给资源命名。</p>
<p>我通常会把资源看作一个盒子。在我们的例子中，这个盒子存储了<strong>训练</strong>集合。</p>
<p>将资源以复数形式命名最大的好处是这对于其他<strong>人类</strong>来说也清晰易懂，复数意味着这是一个包含了各种各样训练的合集。</p>
<p>所以，让我们定义一下路由中端点：</p>
<pre><code class="language-javascript">// 在 src/v1/routes/workoutRoutes.js
const express = require("express");
const router = express.Router();

router.get("/", (req, res) =&gt; {
  res.send("Get all workouts");
});

router.get("/:workoutId", (req, res) =&gt; {
  res.send("Get an existing workout");
});

router.post("/", (req, res) =&gt; {
  res.send("Create a new workout");
});

router.patch("/:workoutId", (req, res) =&gt; {
  res.send("Update an existing workout");
});

router.delete("/:workoutId", (req, res) =&gt; {
  res.send("Delete an existing workout");
});

module.exports = router;
</code></pre>
<p>我们可以删除 <strong>src/v1/routes</strong> 中的测试文件 <strong>index.js</strong> 文件。</p>
<p>现在让我们回到入口接点连接版本 1.0 的路由。</p>
<pre><code class="language-javascript">// 在 src/index.js
const express = require("express");
// *** 删除 ***
const v1Router = require("./v1/routes");
// *** 添加 ***
const v1WorkoutRouter = require("./v1/routes/workoutRoutes");

const app = express();
const PORT = process.env.PORT || 3000;

// *** 删除 ***
app.use("/api/v1", v1Router);

// *** 添加 ***
app.use("/api/v1/workouts", v1WorkoutRouter);

app.listen(PORT, () =&gt; {
  console.log(`API is listening on port ${PORT}`);
});
</code></pre>
<p>进展的很顺利！现在我们就可以通过版本 1 的训练路由捕捉到来自 <strong>/api/v1/workouts</strong> 的所有请求。</p>
<p>在路由当中，我们将调用控制器的方法来处理不同的端点。</p>
<p>让我们为每一个端点创建一个方法。现阶段只需要返回一个信息。</p>
<pre><code class="language-javascript">// 在 src/controllers/workoutController.js
const getAllWorkouts = (req, res) =&gt; {
  res.send("Get all workouts");
};

const getOneWorkout = (req, res) =&gt; {
  res.send("Get an existing workout");
};

const createNewWorkout = (req, res) =&gt; {
  res.send("Create a new workout");
};

const updateOneWorkout = (req, res) =&gt; {
  res.send("Update an existing workout");
};

const deleteOneWorkout = (req, res) =&gt; {
  res.send("Delete an existing workout");
};

module.exports = {
  getAllWorkouts,
  getOneWorkout,
  createNewWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};
</code></pre>
<p>现在可以修改一下训练的路由，调用控制器方法：</p>
<pre><code class="language-javascript">// In src/v1/routes/workoutRoutes.js
const express = require("express");
const workoutController = require("../../controllers/workoutController");

const router = express.Router();

router.get("/", workoutController.getAllWorkouts);

router.get("/:workoutId", workoutController.getOneWorkout);

router.post("/", workoutController.createNewWorkout);

router.patch("/:workoutId", workoutController.updateOneWorkout);

router.delete("/:workoutId", workoutController.deleteOneWorkout);

module.exports = router;
</code></pre>
<p>现在可以测试 <strong>GET /api/v1/workouts/:workoutId</strong> 端点，在浏览器输入 <strong>localhost:3000/api/v1/workouts/2342</strong>，你会看到以下信息：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/Bildschirmfoto-2022-04-30-um-11.29.19.png" alt="Bildschirmfoto-2022-04-30-um-11.29.19" width="600" height="400" loading="lazy"></p>
<p>我们成功了！API 结构的第一层就搭建完毕。让我们用另个最佳实践来创建服务层。</p>
<h3 id="accept-and-respond-with-data-in-json-format">以 JSON 格式接受和响应数据</h3>
<p>和 API 交互的时候，我们会通过请求发送特定数据，或者通过响应接受数据。市面上有各种各样的数据格式，但是 JSON（JavaScript Object Notation）是一个标准格式。</p>
<p>虽然在 JSON 的全称中有 <strong>JavaScript</strong> ，但两者并没有绑定。你也可以使用 Java 或者 Python 来编写你的 API，它们也可以处理 JSON。</p>
<p>由于这样的标准化，API 应该接受和响应 JSON 格式的数据。</p>
<p>让我们回到我们的代码，看看如何把这一点融入到我们的最佳实践。</p>
<p>首先，我们创建服务层。</p>
<pre><code class="language-javascript">// 在src/services/workoutService.js
const getAllWorkouts = () =&gt; {
  return;
};

const getOneWorkout = () =&gt; {
  return;
};

const createNewWorkout = () =&gt; {
  return;
};

const updateOneWorkout = () =&gt; {
  return;
};

const deleteOneWorkout = () =&gt; {
  return;
};

module.exports = {
  getAllWorkouts,
  getOneWorkout,
  createNewWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};
</code></pre>
<p>将服务方法和控制器方法命名为一样的名字也是一种最佳实践，这样可以让两者保持关联。让我们先不返回任何东西。</p>
<p>在训练的控制器中，调用服务层的这些方法：</p>
<pre><code class="language-javascript">// 在src/controllers/workoutController.js
// *** 添加 ***
const workoutService = require("../services/workoutService");

const getAllWorkouts = (req, res) =&gt; {
  // *** 添加 ***
  const allWorkouts = workoutService.getAllWorkouts();
  res.send("Get all workouts");
};

const getOneWorkout = (req, res) =&gt; {
  // *** 添加 ***
  const workout = workoutService.getOneWorkout();
  res.send("Get an existing workout");
};

const createNewWorkout = (req, res) =&gt; {
  // *** 添加 ***
  const createdWorkout = workoutService.createNewWorkout();
  res.send("Create a new workout");
};

const updateOneWorkout = (req, res) =&gt; {
  // *** 添加 ***
  const updatedWorkout = workoutService.updateOneWorkout();
  res.send("Update an existing workout");
};

const deleteOneWorkout = (req, res) =&gt; {
  // *** 添加 ***
  workoutService.deleteOneWorkout();
  res.send("Delete an existing workout");
};

module.exports = {
  getAllWorkouts,
  getOneWorkout,
  createNewWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};
</code></pre>
<p>我们暂且不需要改变控制器响应中的任何内容，但是控制器已经可以和服务层联通了。</p>
<p>在服务的方法中，我们处理了业务逻辑，如改变数据结构以及和数据层交互。</p>
<p>为此我们需要创建一个数据层和一组处理与数据库交互的方法。我们的数据库将为简单的训练 JSON 文件。</p>
<pre><code class="language-bash"># 在src/database中创建一个新的名为 db.json的文件
touch src/database/db.json 

# 在/src/database中创建一个存储所有训练相关方法的文件
touch src/database/Workout.js
</code></pre>
<p>将这些内容复制粘贴到 db.json：</p>
<pre><code class="language-json">{
  "workouts": [
    {
      "id": "61dbae02-c147-4e28-863c-db7bd402b2d6",
      "name": "Tommy V",
      "mode": "For Time",
      "equipment": [
        "barbell",
        "rope"
      ],
      "exercises": [
        "21 thrusters",
        "12 rope climbs, 15 ft",
        "15 thrusters",
        "9 rope climbs, 15 ft",
        "9 thrusters",
        "6 rope climbs, 15 ft"
      ],
      "createdAt": "4/20/2022, 2:21:56 PM",
      "updatedAt": "4/20/2022, 2:21:56 PM",
      "trainerTips": [
        "Split the 21 thrusters as needed",
        "Try to do the 9 and 6 thrusters unbroken",
        "RX Weights: 115lb/75lb"
      ]
    },
    {
      "id": "4a3d9aaa-608c-49a7-a004-66305ad4ab50",
      "name": "Dead Push-Ups",
      "mode": "AMRAP 10",
      "equipment": [
        "barbell"
      ],
      "exercises": [
        "15 deadlifts",
        "15 hand-release push-ups"
      ],
      "createdAt": "1/25/2022, 1:15:44 PM",
      "updatedAt": "3/10/2022, 8:21:56 AM",
      "trainerTips": [
        "Deadlifts are meant to be light and fast",
        "Try to aim for unbroken sets",
        "RX Weights: 135lb/95lb"
      ]
    },
    {
      "id": "d8be2362-7b68-4ea4-a1f6-03f8bc4eede7",
      "name": "Heavy DT",
      "mode": "5 Rounds For Time",
      "equipment": [
        "barbell",
        "rope"
      ],
      "exercises": [
        "12 deadlifts",
        "9 hang power cleans",
        "6 push jerks"
      ],
      "createdAt": "11/20/2021, 5:39:07 PM",
      "updatedAt": "11/20/2021, 5:39:07 PM",
      "trainerTips": [
        "Aim for unbroken push jerks",
        "The first three rounds might feel terrible, but stick to it",
        "RX Weights: 205lb/145lb"
      ]
    }
  ]
}
</code></pre>
<p>可以看到上面添加了三组训练数据。每组训练包含 id、name、mode、equipment、exercises、createdAt、updatedAt 和 trainerTips。</p>
<p>我们从最简单的开始，返回所有存储的训练，在访问数据层建立对应的方法（src/database/Workout.js）。</p>
<p>在这里我也使用和服务层、控制器的相同的命名，不过是否这样命名完全取决于你的选择。</p>
<pre><code class="language-javascript">// 在src/database/Workout.js
const DB = require("./db.json");

const getAllWorkouts = () =&gt; {
  return DB.workouts;
};

module.exports = { getAllWorkouts };
</code></pre>
<p>回到训练计划服务层，实现 <strong>getAllWorkouts</strong> 的逻辑。</p>
<pre><code class="language-javascript">// 在src/database/workoutService.js
// *** 添加 ***
const Workout = require("../database/Workout");
const getAllWorkouts = () =&gt; {
  // *** 添加 ***
  const allWorkouts = Workout.getAllWorkouts();
  // *** 添加 ***
  return allWorkouts;
};

const getOneWorkout = () =&gt; {
  return;
};

const createNewWorkout = () =&gt; {
  return;
};

const updateOneWorkout = () =&gt; {
  return;
};

const deleteOneWorkout = () =&gt; {
  return;
};

module.exports = {
  getAllWorkouts,
  getOneWorkout,
  createNewWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};
</code></pre>
<p>返回所有的训练十分简单，我们不需要改变数据格式，因为数据已经是一个 JSON 文件了。我们暂时也不需要传入参数，现在做的事情非常简单直白，待会儿我们会重新回到这里。</p>
<p>在我们的训练控制器中，已经接受到了 <code>workoutService.getAllWorkouts()</code> 的返回值，并作为响应发送给客户端。我们完成了数据库从服务层到控制器的响应循环。</p>
<pre><code class="language-javascript">// 在src/controllers/workoutControllers.js
const workoutService = require("../services/workoutService");

const getAllWorkouts = (req, res) =&gt; {
  const allWorkouts = workoutService.getAllWorkouts();
  // *** 添加 ***
  res.send({ status: "OK", data: allWorkouts });
};

const getOneWorkout = (req, res) =&gt; {
  const workout = workoutService.getOneWorkout();
  res.send("Get an existing workout");
};

const createNewWorkout = (req, res) =&gt; {
  const createdWorkout = workoutService.createNewWorkout();
  res.send("Create a new workout");
};

const updateOneWorkout = (req, res) =&gt; {
  const updatedWorkout = workoutService.updateOneWorkout();
  res.send("Update an existing workout");
};

const deleteOneWorkout = (req, res) =&gt; {
  workoutService.deleteOneWorkout();
  res.send("Delete an existing workout");
};

module.exports = {
  getAllWorkouts,
  getOneWorkout,
  createNewWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};
</code></pre>
<p>在浏览器访问 <strong>localhost:3000/api/v1/workouts</strong>，你将看到响应的 JSON。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/Bildschirmfoto-2022-04-30-um-11.38.14.png" alt="Bildschirmfoto-2022-04-30-um-11.38.14" width="600" height="400" loading="lazy"></p>
<p>一切都进展得很顺利，我们将数据以 JSON 的形式返回。但如何接受来自客户端的数据呢？假设我们需要一个端点来接受来自客户端的 JSON，在这个端点客户端创建和更新训练数据。</p>
<p>在控制器中，我们提取了请求体来创建一个新的训练，并传入训练服务层。在训练服务层，我们插入了 DB.json 并且将新创建的训练返回到客户端。</p>
<p>要想在请求体中解析 JSON，我们需要首先安装 <strong>body-parser</strong> 并配置。</p>
<pre><code class="language-bash">npm i body-parser
</code></pre>
<pre><code class="language-javascript">// 在src/index.js中
const express = require("express");
// *** 添加 ***
const bodyParser = require("body-parser");
const v1WorkoutRouter = require("./v1/routes/workoutRoutes");

const app = express();
const PORT = process.env.PORT || 3000;

// *** 添加 ***
app.use(bodyParser.json());
app.use("/api/v1/workouts", v1WorkoutRouter);

app.listen(PORT, () =&gt; {
  console.log(`API is listening on port ${PORT}`);
});
</code></pre>
<p>现在我们就可以在控制器的 <strong>req.body</strong> 中接受 JSON 格式的数据。</p>
<p>可以打开你最喜欢的 HTTP 服务器（我使用的是 Postman）来进行测试，创建一个路由为 localhost:3000/api/v1/workouts的POST请求，并且将请求体设置为 JSON 格式：</p>
<pre><code class="language-javascript">{
  "name": "Core Buster",
  "mode": "AMRAP 20",
  "equipment": [
    "rack",
    "barbell",
    "abmat"
  ],
  "exercises": [
    "15 toes to bars",
    "10 thrusters",
    "30 abmat sit-ups"
  ],
  "trainerTips": [
    "Split your toes to bars into two sets maximum",
    "Go unbroken on the thrusters",
    "Take the abmat sit-ups as a chance to normalize your breath"
  ]
}
</code></pre>
<p>你可能注意到了 “id”、“createdAt”、“updatedAt” 这些属性不存在。添加这些属性是我们 API 的工作，我们会在训练服务层中处理相关内容。</p>
<p>在训练控制器的 <strong>createNewWorkout</strong> 方法中，我们可以在请求体中提取 body，并做一些验证，并作为参数传入训练服务层。</p>
<pre><code class="language-javascript">// 在src/controllers/workoutController.js
...

const createNewWorkout = (req, res) =&gt; {
  const { body } = req;
  // *** 添加 ***
  if (
    !body.name ||
    !body.mode ||
    !body.equipment ||
    !body.exercises ||
    !body.trainerTips
  ) {
    return;
  }
  // *** 添加 ***
  const newWorkout = {
    name: body.name,
    mode: body.mode,
    equipment: body.equipment,
    exercises: body.exercises,
    trainerTips: body.trainerTips,
  };
  // *** 添加 ***
  const createdWorkout = workoutService.createNewWorkout(newWorkout);
  // *** 添加 ***
  res.status(201).send({ status: "OK", data: createdWorkout });
};

...
</code></pre>
<p>通常会使用第三方包来来提升请求验证性能，如：<a href="https://express-validator.github.io/docs/">express-validator</a>。</p>
<p>训练服务层接受来自 createdNewWorkout 方法传入的数据。</p>
<p>之后我们将缺失的属性传入对象，并将这个对象作为新的方法传入数据访问层，再存入 DB 中。</p>
<p>首先我们要创建一个简单的 Util 函数，来覆盖 JSON 文件以实时更新数据：</p>
<pre><code class="language-bash"># 在 data 目录下创建 util 文件
touch src/database/utils.js
</code></pre>
<pre><code class="language-javascript">// 在 src/database/utils.js
const fs = require("fs");

const saveToDatabase = (DB) =&gt; {
  fs.writeFileSync("./src/database/db.json", JSON.stringify(DB, null, 2), {
    encoding: "utf-8",
  });
};

module.exports = { saveToDatabase };
</code></pre>
<p>我们可以在 Workout.js 文件中使用这个函数：</p>
<pre><code class="language-javascript">// 在src/database/Workout.js
const DB = require("./db.json");
// *** 添加 ***
const { saveToDatabase } = require("./utils");


const getAllWorkouts = () =&gt; {
  return DB.workouts;
};

// *** 添加 ***
const createNewWorkout = (newWorkout) =&gt; {
  const isAlreadyAdded =
    DB.workouts.findIndex((workout) =&gt; workout.name === newWorkout.name) &gt; -1;
  if (isAlreadyAdded) {
    return;
  }
  DB.workouts.push(newWorkout);
  saveToDatabase(DB);
  return newWorkout;
};

module.exports = {
  getAllWorkouts,
  // *** 添加 ***
  createNewWorkout,
};
</code></pre>
<p>一切进展得很顺利。下一步是调用训练服务层中的数据库方法。</p>
<pre><code class="language-bash"># 安装 uuid 包
npm i uuid
</code></pre>
<pre><code class="language-javascript">// 在src/services/workoutService.js
// *** 添加 ***
const { v4: uuid } = require("uuid");
// *** 添加 ***
const Workout = require("../database/Workout");

const getAllWorkouts = () =&gt; {
  const allWorkouts = Workout.getAllWorkouts()
  return allWorkouts;
};

const getOneWorkout = () =&gt; {
  return;
};

const createNewWorkout = (newWorkout) =&gt; {
  // *** 添加 ***
  const workoutToInsert = {
    ...newWorkout,
    id: uuid(),
    createdAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
    updatedAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
  };
  // *** 添加 ***
  const createdWorkout = Workout.createNewWorkout(workoutToInsert);
  return createdWorkout;
};

const updateOneWorkout = () =&gt; {
  return;
};

const deleteOneWorkout = () =&gt; {
  return;
};

module.exports = {
  getAllWorkouts,
  getOneWorkout,
  createNewWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};
</code></pre>
<p>一切还不错，对不对？现在你可以去 HTTP 客户端，重新发送 POST 请求，就会接受到新的 JSON 格式的训练。</p>
<p>如果你尝试再次添加同样的训练，你仍会得到 201 状态码，但是不会插入新的内容。</p>
<p>也就是说我们的数据库方法取消了插入，什么都不返回。这是因为 if 声明检查了是否已经存在同样名称的内容，暂时这么处理，我们会在下一个最佳实践中讲解如何优化。</p>
<p>现在向 <strong>localhost:3000/api/v1/workouts</strong> 发出 GET 请求，读取所有的训练。我选择使用浏览器来操作，你会看到我们的训练成功地插入了：<br>
<img src="https://www.freecodecamp.org/news/content/images/2022/04/Bildschirmfoto-2022-04-30-um-11.57.23.png" alt="Bildschirmfoto-2022-04-30-um-11.57.23" width="600" height="400" loading="lazy"></p>
<p>你可以选择自行编写其他的方法，或者直接复制我的。</p>
<p>首先是训练控制器（你可以直接复制所有内容）。</p>
<pre><code class="language-javascript">// 在src/controllers/workoutController.js
const workoutService = require("../services/workoutService");

const getAllWorkouts = (req, res) =&gt; {
  const allWorkouts = workoutService.getAllWorkouts();
  res.send({ status: "OK", data: allWorkouts });
};

const getOneWorkout = (req, res) =&gt; {
  const {
    params: { workoutId },
  } = req;
  if (!workoutId) {
    return;
  }
  const workout = workoutService.getOneWorkout(workoutId);
  res.send({ status: "OK", data: workout });
};

const createNewWorkout = (req, res) =&gt; {
  const { body } = req;
  if (
    !body.name ||
    !body.mode ||
    !body.equipment ||
    !body.exercises ||
    !body.trainerTips
  ) {
    return;
  }
  const newWorkout = {
    name: body.name,
    mode: body.mode,
    equipment: body.equipment,
    exercises: body.exercises,
    trainerTips: body.trainerTips,
  };
  const createdWorkout = workoutService.createNewWorkout(newWorkout);
  res.status(201).send({ status: "OK", data: createdWorkout });
};

const updateOneWorkout = (req, res) =&gt; {
  const {
    body,
    params: { workoutId },
  } = req;
  if (!workoutId) {
    return;
  }
  const updatedWorkout = workoutService.updateOneWorkout(workoutId, body);
  res.send({ status: "OK", data: updatedWorkout });
};

const deleteOneWorkout = (req, res) =&gt; {
  const {
    params: { workoutId },
  } = req;
  if (!workoutId) {
    return;
  }
  workoutService.deleteOneWorkout(workoutId);
  res.status(204).send({ status: "OK" });
};

module.exports = {
  getAllWorkouts,
  getOneWorkout,
  createNewWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};
</code></pre>
<p>然后是训练服务层（你可以直接复制所有内容）。</p>
<pre><code class="language-javascript">// 在 src/services/workoutServices.js
const { v4: uuid } = require("uuid");
const Workout = require("../database/Workout");

const getAllWorkouts = () =&gt; {
  const allWorkouts = Workout.getAllWorkouts();
  return allWorkouts;
};

const getOneWorkout = (workoutId) =&gt; {
  const workout = Workout.getOneWorkout(workoutId);
  return workout;
};

const createNewWorkout = (newWorkout) =&gt; {
  const workoutToInsert = {
    ...newWorkout,
    id: uuid(),
    createdAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
    updatedAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
  };
  const createdWorkout = Workout.createNewWorkout(workoutToInsert);
  return createdWorkout;
};

const updateOneWorkout = (workoutId, changes) =&gt; {
  const updatedWorkout = Workout.updateOneWorkout(workoutId, changes);
  return updatedWorkout;
};

const deleteOneWorkout = (workoutId) =&gt; {
  Workout.deleteOneWorkout(workoutId);
};

module.exports = {
  getAllWorkouts,
  getOneWorkout,
  createNewWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};
</code></pre>
<p>最后是数据访问层的数据库方法（你可以直接复制所有内容）。</p>
<pre><code class="language-javascript">// 在src/database/Workout.js
const DB = require("./db.json");
const { saveToDatabase } = require("./utils");

const getAllWorkouts = () =&gt; {
  return DB.workouts;
};

const getOneWorkout = (workoutId) =&gt; {
  const workout = DB.workouts.find((workout) =&gt; workout.id === workoutId);
  if (!workout) {
    return;
  }
  return workout;
};

const createNewWorkout = (newWorkout) =&gt; {
  const isAlreadyAdded =
    DB.workouts.findIndex((workout) =&gt; workout.name === newWorkout.name) &gt; -1;
  if (isAlreadyAdded) {
    return;
  }
  DB.workouts.push(newWorkout);
  saveToDatabase(DB);
  return newWorkout;
};

const updateOneWorkout = (workoutId, changes) =&gt; {
  const indexForUpdate = DB.workouts.findIndex(
    (workout) =&gt; workout.id === workoutId
  );
  if (indexForUpdate === -1) {
    return;
  }
  const updatedWorkout = {
    ...DB.workouts[indexForUpdate],
    ...changes,
    updatedAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
  };
  DB.workouts[indexForUpdate] = updatedWorkout;
  saveToDatabase(DB);
  return updatedWorkout;
};

const deleteOneWorkout = (workoutId) =&gt; {
  const indexForDeletion = DB.workouts.findIndex(
    (workout) =&gt; workout.id === workoutId
  );
  if (indexForDeletion === -1) {
    return;
  }
  DB.workouts.splice(indexForDeletion, 1);
  saveToDatabase(DB);
};

module.exports = {
  getAllWorkouts,
  createNewWorkout,
  getOneWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};
</code></pre>
<p>太棒了！让我们进入下一个最佳实践，来看看怎么处理报错。</p>
<h3 id="respond-with-standard-http-error-codes">响应标准 HTTP 错误代码</h3>
<p>我们已经完成了不少内容的搭建，但还没结束呢。现在我们的 API 已经可以处理 CRUD 并且存储数据，这样很棒！但还不够。</p>
<p>为什么？让我来解释。</p>
<p>在一个完美的世界里，所有事情都会运行顺利，没有错误。但是你可能知道，在现实中会出现很多错误——无论这个错误是人为的还是是技术角度。</p>
<p>你或许也认为从一开始就没有任何错误是一种奇怪的感觉，这样确实很棒也让人享受，但作为一个开发者，我们应该更习惯与错误共处。😁</p>
<p>API 也是这样，我们需要处理出现问题或者报错的情况。这也可以使我门的 API 更强大。</p>
<p>出现问题时（不论是在请求中还是在我们 API 内部），我们返回 HTTP 错误代码。我见过并使用过一些 API 始终返回 400 错误代码，并且不附带任何具体的信息说明为什么错误会出现，错误是什么。这样调试起来就很痛苦。</p>
<p>这就是为什么针对不同的情况返回合适的 HTTP 代码是一种最佳实践。这能够使正在使用或者构建 API 的工程师更轻松地识别问题。</p>
<p>为了提升体验，我们还可以在返回错误的同时快速发送一个错误信息。但正如在文章开头说的那样，这一做法并不是万精油，还需要工程师自己来权衡。</p>
<p>例如，是否应该向用户返回 <strong>“该用户名已经注册”</strong> 这类信息是需要深思熟虑的，因为或许这样就给用户提供了本该隐藏的数据。</p>
<p>可以浏览一遍交叉训练 API 中的创建（CRUD 中的 C）端点，看看会出现什么问题，我们能怎么解决。在这一部分最后部分有其他端点的完整实现。</p>
<p>我们先从训练控制器的 createNewWorkout 方法开始：</p>
<pre><code class="language-javascript">// 在src/controllers/workoutController.js
...

const createNewWorkout = (req, res) =&gt; {
  const { body } = req;
  if (
    !body.name ||
    !body.mode ||
    !body.equipment ||
    !body.exercises ||
    !body.trainerTips
  ) {
    return;
  }
  const newWorkout = {
    name: body.name,
    mode: body.mode,
    equipment: body.equipment,
    exercises: body.exercises,
    trainerTips: body.trainerTips,
  };
  const createdWorkout = workoutService.createNewWorkout(newWorkout);
  res.status(201).send({ status: "OK", data: createdWorkout });
};

...
</code></pre>
<p>我们的代码已经可以捕获请求体属性不完整的情况。</p>
<p>在返回 400 时，附带一条返回错误信息是一个不错的选择。</p>
<pre><code class="language-javascript">// 在src/controllers/workoutController.js
...

const createNewWorkout = (req, res) =&gt; {
  const { body } = req;
  if (
    !body.name ||
    !body.mode ||
    !body.equipment ||
    !body.exercises ||
    !body.trainerTips
  ) {
    res
      .status(400)
      .send({
        status: "FAILED",
        data: {
          error:
            "One of the following keys is missing or is empty in request body: 'name', 'mode', 'equipment', 'exercises', 'trainerTips'",
        },
      });
    return;
  }
  const newWorkout = {
    name: body.name,
    mode: body.mode,
    equipment: body.equipment,
    exercises: body.exercises,
    trainerTips: body.trainerTips,
  };
  const createdWorkout = workoutService.createNewWorkout(newWorkout);
  res.status(201).send({ status: "OK", data: createdWorkout });
};

...
</code></pre>
<p>如果我们想要添加一个新的训练，但是忘记在请求体提供 “mode” 属性，我们会在 400 报错的同时看到错误信息。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/Bildschirmfoto-2022-04-30-um-15.17.21.png" alt="Bildschirmfoto-2022-04-30-um-15.17.21" width="600" height="400" loading="lazy"></p>
<p>这样的话，使用这个 API 的开发者就更知道自己需要什么。他们马上就知道应该在请求体中找答案，并且看看他们缺失了哪一个必须的属性。</p>
<p>在我们的例子中使用通用的错误信息没有问题。一般情况下可以使用一个模式验证器来处理这个问题。</p>
<p>让我们再深入一层看看服务层有什么潜在的错误：</p>
<pre><code class="language-javascript">// 在src/services/workoutService.js
...

const createNewWorkout = (newWorkout) =&gt; {
  const workoutToInsert = {
    ...newWorkout,
    id: uuid(),
    createdAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
    updatedAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
  };
  const createdWorkout = Workout.createNewWorkout(workoutToInsert);
  return createdWorkout;
};

...
</code></pre>
<p>在 <strong>Workout.createNewWorkout()</strong> 中的插入数据可能出现问题，我想将它们打包在 try/catch 代码块中，来捕获错误。</p>
<pre><code class="language-javascript">// 在src/services/workoutService.js
...

const createNewWorkout = (newWorkout) =&gt; {
  const workoutToInsert = {
    ...newWorkout,
    id: uuid(),
    createdAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
    updatedAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
  };
  try {
    const createdWorkout = Workout.createNewWorkout(workoutToInsert);
    return createdWorkout;
  } catch (error) {
    throw error;
  }
};

...
</code></pre>
<p>Workout.createNewWorkout() 方法中的所有错误都会被 catch 代码块捕获。我们抛出这个错误之后就可以在控制器中调整响应。</p>
<p>让我们在 Workout.js 中定义错误：</p>
<pre><code class="language-javascript">// 在src/database/Workout.js
...

const createNewWorkout = (newWorkout) =&gt; {
  const isAlreadyAdded =
    DB.workouts.findIndex((workout) =&gt; workout.name === newWorkout.name) &gt; -1;
  if (isAlreadyAdded) {
    throw {
      status: 400,
      message: `Workout with the name '${newWorkout.name}' already exists`,
    };
  }
  try {
    DB.workouts.push(newWorkout);
    saveToDatabase(DB);
    return newWorkout;
  } catch (error) {
    throw { status: 500, message: error?.message || error };
  }
};

...
</code></pre>
<p>如你所见，一个错误包含了状态和信息两个内容。 此处我使用了 <strong>throw</strong> 关键字来抛出一个数据结构而不是一条字符串，<strong>throw new Error()</strong> 必须这么写。</p>
<p>使用 throw 的缺点是无法得到栈追踪。但基本上抛出错误由第三方库来处理（如果你使用 MongoDB 数据库的话就是 Mongoose），在本教程中，我们现在做的就足够了。</p>
<p>现在我们就可以在服务和数据访问层来抛出和捕获错误了。我们现在进入训练控制层，来编写抛出错误和对应的消息。</p>
<pre><code class="language-javascript">// 在src/controllers/workoutController.js
...

const createNewWorkout = (req, res) =&gt; {
  const { body } = req;
  if (
    !body.name ||
    !body.mode ||
    !body.equipment ||
    !body.exercises ||
    !body.trainerTips
  ) {
    res
      .status(400)
      .send({
        status: "FAILED",
        data: {
          error:
            "One of the following keys is missing or is empty in request body: 'name', 'mode', 'equipment', 'exercises', 'trainerTips'",
        },
      });
    return;
  }
  const newWorkout = {
    name: body.name,
    mode: body.mode,
    equipment: body.equipment,
    exercises: body.exercises,
    trainerTips: body.trainerTips,
  };
  // *** 添加 ***
  try {
    const createdWorkout = workoutService.createNewWorkout(newWorkout);
    res.status(201).send({ status: "OK", data: createdWorkout });
  } catch (error) {
    res
      .status(error?.status || 500)
      .send({ status: "FAILED", data: { error: error?.message || error } });
  }
};

...
</code></pre>
<p>你可以通过添加同样名字的训练，或者不在请求体中提供必需的属性来测试。你会接受对应的 HTTP 错误代码以及错误信息。</p>
<p>在结束这一篇并且进入下一个最佳实践之前，让我们复制其他的实现代码，或者你可以尝试自己编写：</p>
<pre><code class="language-javascript">// 在src/controllers/workoutController.js
const workoutService = require("../services/workoutService");

const getAllWorkouts = (req, res) =&gt; {
  try {
    const allWorkouts = workoutService.getAllWorkouts();
    res.send({ status: "OK", data: allWorkouts });
  } catch (error) {
    res
      .status(error?.status || 500)
      .send({ status: "FAILED", data: { error: error?.message || error } });
  }
};

const getOneWorkout = (req, res) =&gt; {
  const {
    params: { workoutId },
  } = req;
  if (!workoutId) {
    res
      .status(400)
      .send({
        status: "FAILED",
        data: { error: "Parameter ':workoutId' can not be empty" },
      });
  }
  try {
    const workout = workoutService.getOneWorkout(workoutId);
    res.send({ status: "OK", data: workout });
  } catch (error) {
    res
      .status(error?.status || 500)
      .send({ status: "FAILED", data: { error: error?.message || error } });
  }
};

const createNewWorkout = (req, res) =&gt; {
  const { body } = req;
  if (
    !body.name ||
    !body.mode ||
    !body.equipment ||
    !body.exercises ||
    !body.trainerTips
  ) {
    res
      .status(400)
      .send({
        status: "FAILED",
        data: {
          error:
            "One of the following keys is missing or is empty in request body: 'name', 'mode', 'equipment', 'exercises', 'trainerTips'",
        },
      });
    return;
  }
  const newWorkout = {
    name: body.name,
    mode: body.mode,
    equipment: body.equipment,
    exercises: body.exercises,
    trainerTips: body.trainerTips,
  };
  try {
    const createdWorkout = workoutService.createNewWorkout(newWorkout);
    res.status(201).send({ status: "OK", data: createdWorkout });
  } catch (error) {
    res
      .status(error?.status || 500)
      .send({ status: "FAILED", data: { error: error?.message || error } });
  }
};

const updateOneWorkout = (req, res) =&gt; {
  const {
    body,
    params: { workoutId },
  } = req;
  if (!workoutId) {
    res
      .status(400)
      .send({
        status: "FAILED",
        data: { error: "Parameter ':workoutId' can not be empty" },
      });
  }
  try {
    const updatedWorkout = workoutService.updateOneWorkout(workoutId, body);
    res.send({ status: "OK", data: updatedWorkout });
  } catch (error) {
    res
      .status(error?.status || 500)
      .send({ status: "FAILED", data: { error: error?.message || error } });
  }
};

const deleteOneWorkout = (req, res) =&gt; {
  const {
    params: { workoutId },
  } = req;
  if (!workoutId) {
    res
      .status(400)
      .send({
        status: "FAILED",
        data: { error: "Parameter ':workoutId' can not be empty" },
      });
  }
  try {
    workoutService.deleteOneWorkout(workoutId);
    res.status(204).send({ status: "OK" });
  } catch (error) {
    res
      .status(error?.status || 500)
      .send({ status: "FAILED", data: { error: error?.message || error } });
  }
};

module.exports = {
  getAllWorkouts,
  getOneWorkout,
  createNewWorkout,
  updateOneWorkout,
  deleteOneWorkout,
  getRecordsForWorkout,
};
</code></pre>
<pre><code class="language-javascript">// In src/services/workoutService.js
const { v4: uuid } = require("uuid");
const Workout = require("../database/Workout");

const getAllWorkouts = () =&gt; {
  try {
    const allWorkouts = Workout.getAllWorkouts();
    return allWorkouts;
  } catch (error) {
    throw error;
  }
};

const getOneWorkout = (workoutId) =&gt; {
  try {
    const workout = Workout.getOneWorkout(workoutId);
    return workout;
  } catch (error) {
    throw error;
  }
};

const createNewWorkout = (newWorkout) =&gt; {
  const workoutToInsert = {
    ...newWorkout,
    id: uuid(),
    createdAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
    updatedAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
  };
  try {
    const createdWorkout = Workout.createNewWorkout(workoutToInsert);
    return createdWorkout;
  } catch (error) {
    throw error;
  }
};

const updateOneWorkout = (workoutId, changes) =&gt; {
  try {
    const updatedWorkout = Workout.updateOneWorkout(workoutId, changes);
    return updatedWorkout;
  } catch (error) {
    throw error;
  }
};

const deleteOneWorkout = (workoutId) =&gt; {
  try {
    Workout.deleteOneWorkout(workoutId);
  } catch (error) {
    throw error;
  }
};

module.exports = {
  getAllWorkouts,
  getOneWorkout,
  createNewWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};
</code></pre>
<pre><code class="language-javascript">// 在src/database/Workout.js
const DB = require("./db.json");
const { saveToDatabase } = require("./utils");

const getAllWorkouts = () =&gt; {
  try {
    return DB.workouts;
  } catch (error) {
    throw { status: 500, message: error };
  }
};

const getOneWorkout = (workoutId) =&gt; {
  try {
    const workout = DB.workouts.find((workout) =&gt; workout.id === workoutId);
    if (!workout) {
      throw {
        status: 400,
        message: `Can't find workout with the id '${workoutId}'`,
      };
    }
    return workout;
  } catch (error) {
    throw { status: error?.status || 500, message: error?.message || error };
  }
};

const createNewWorkout = (newWorkout) =&gt; {
  try {
    const isAlreadyAdded =
      DB.workouts.findIndex((workout) =&gt; workout.name === newWorkout.name) &gt; -1;
    if (isAlreadyAdded) {
      throw {
        status: 400,
        message: `Workout with the name '${newWorkout.name}' already exists`,
      };
    }
    DB.workouts.push(newWorkout);
    saveToDatabase(DB);
    return newWorkout;
  } catch (error) {
    throw { status: error?.status || 500, message: error?.message || error };
  }
};

const updateOneWorkout = (workoutId, changes) =&gt; {
  try {
    const isAlreadyAdded =
      DB.workouts.findIndex((workout) =&gt; workout.name === changes.name) &gt; -1;
    if (isAlreadyAdded) {
      throw {
        status: 400,
        message: `Workout with the name '${changes.name}' already exists`,
      };
    }
    const indexForUpdate = DB.workouts.findIndex(
      (workout) =&gt; workout.id === workoutId
    );
    if (indexForUpdate === -1) {
      throw {
        status: 400,
        message: `Can't find workout with the id '${workoutId}'`,
      };
    }
    const updatedWorkout = {
      ...DB.workouts[indexForUpdate],
      ...changes,
      updatedAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
    };
    DB.workouts[indexForUpdate] = updatedWorkout;
    saveToDatabase(DB);
    return updatedWorkout;
  } catch (error) {
    throw { status: error?.status || 500, message: error?.message || error };
  }
};

const deleteOneWorkout = (workoutId) =&gt; {
  try {
    const indexForDeletion = DB.workouts.findIndex(
      (workout) =&gt; workout.id === workoutId
    );
    if (indexForDeletion === -1) {
      throw {
        status: 400,
        message: `Can't find workout with the id '${workoutId}'`,
      };
    }
    DB.workouts.splice(indexForDeletion, 1);
    saveToDatabase(DB);
  } catch (error) {
    throw { status: error?.status || 500, message: error?.message || error };
  }
};

module.exports = {
  getAllWorkouts,
  createNewWorkout,
  getOneWorkout,
  updateOneWorkout,
  deleteOneWorkout,
};
</code></pre>
<h3 id="avoid-verbs-in-endpoint-names">避免在端点使用动词</h3>
<p>在端点中使用动词实际上没有任何作用。大体上 URL 和资源（想想我们前文提到的“盒子”）是一一对应的。</p>
<p>在 URL 中使用动词，相当于展示了资源本身并没有的行为。</p>
<p>我们已经在不使用动词的情况下正确地编写好了 URL，但让我们看看，如果使用动词，URL 会是什么样。</p>
<pre><code class="language-javascript">// 现在的样子（没有动词）
GET "/api/v1/workouts" 
GET "/api/v1/workouts/:workoutId" 
POST "/api/v1/workouts" 
PATCH "/api/v1/workouts/:workoutId" 
DELETE "/api/v1/workouts/:workoutId"  

// 使用动词
GET "/api/v1/getAllWorkouts" 
GET "/api/v1/getWorkoutById/:workoutId" 
CREATE "/api/v1/createWorkout" 
PATCH "/api/v1/updateWorkout/:workoutId" 
DELETE "/api/v1/deleteWorkout/:workoutId"
</code></pre>
<p>你看到区别了吗？给每一个行为分配不同的 URL，会让人困惑并且十分复杂。</p>
<p>假设我们有 300 个不同的端点。为每个端点分配单独的 URL 可能造成开销（和文档）地狱。</p>
<p>另一个我不推荐在 URL 中使用动词的原因是，HTTP 动词已经表明了响应的动作。</p>
<p>如 <strong>“GET /api/v1/getAllWorkouts”</strong> 和 <strong>“DELETE api/v1/deleteWorkout/workoutId”</strong> 就很没有必要。</p>
<p>你会发现我们的实现非常清晰，因为我们只使用两个不同的 URL，而实际的行为是通过 HTTP 动词以及对应的请求有效载荷来实现。</p>
<p>我认为 HTTP 动词是来定义行为的（我们也希望这样），而 URL（指向资源）是目标。<strong>“GET /api/v1/workouts”</strong> 这句话即便是人类的语言中也更通顺。</p>
<h3 id="group-associated-resources-together-logical-nesting-">把相关的资源放在一起（逻辑嵌套）</h3>
<p>当你在设计 API 的时候，会出现资源之间相互关联的情况。一个好的实践方式是将资源整合和嵌套到一个端点。</p>
<p>在我们的 API 中，有一系列的会员注册了交叉训练盒子（此处的“盒子”是交叉训练健身房的名字），为了鼓励会员，我们记录了每一次训练的所有记录。</p>
<p>假设有一组训练包含一定顺序的练习，你想要尽快做完。我们记录了所有会员完成这项训练的时间。</p>
<p>这时，前端就需要一个端点来响应一个特定训练的所有时间记录，并且在 UI 上呈现。</p>
<p>训练、会员还有训练记录存储在不同的数据库里。所以在这里我们需要使用盒中盒（训练中的记录），对不对？</p>
<p>这个端点的 URI 会是 <strong>/api/v1/workouts/:workoutId/records</strong>。这便是一个在 URL 中实现逻辑嵌套的好实践。URL 本身不需要反应数据结构。</p>
<p>让我们来实现这个端点。</p>
<p>首先我们要在 db.json 中添加一组叫 “memebers” 的数据，放在 “workouts” 下面。</p>
<pre><code class="language-json">{
  "workouts": [ ...
  ],
  "members": [
    {
      "id": "12a410bc-849f-4e7e-bfc8-4ef283ee4b19",
      "name": "Jason Miller",
      "gender": "male",
      "dateOfBirth": "23/04/1990",
      "email": "jason@mail.com",
      "password": "666349420ec497c1dc890c45179d44fb13220239325172af02d1fb6635922956"
    },
    {
      "id": "2b9130d4-47a7-4085-800e-0144f6a46059",
      "name": "Tiffany Brookston",
      "gender": "female",
      "dateOfBirth": "09/06/1996",
      "email": "tiffy@mail.com",
      "password": "8a1ea5669b749354110dcba3fac5546c16e6d0f73a37f35a84f6b0d7b3c22fcc"
    },
    {
      "id": "11817fb1-03a1-4b4a-8d27-854ac893cf41",
      "name": "Catrin Stevenson",
      "gender": "female",
      "dateOfBirth": "17/08/2001",
      "email": "catrin@mail.com",
      "password": "18eb2d6c5373c94c6d5d707650d02c3c06f33fac557c9cfb8cb1ee625a649ff3"
    },
    {
      "id": "6a89217b-7c28-4219-bd7f-af119c314159",
      "name": "Greg Bronson",
      "gender": "male",
      "dateOfBirth": "08/04/1993",
      "email": "greg@mail.com",
      "password": "a6dcde7eceb689142f21a1e30b5fdb868ec4cd25d5537d67ac7e8c7816b0e862"
    }
  ]
}
</code></pre>
<p>在你问之前，我先回答——是的，密码是哈希加密的。😉</p>
<p>然后我们在 “records” 下面添加 “members”：</p>
<pre><code class="language-json">{
  "workouts": [ ...
  ],
  "members": [ ...
  ],
  "records": [
    {
      "id": "ad75d475-ac57-44f4-a02a-8f6def58ff56",
      "workout": "4a3d9aaa-608c-49a7-a004-66305ad4ab50",
      "record": "160 reps"
    },
    {
      "id": "0bff586f-2017-4526-9e52-fe3ea46d55ab",
      "workout": "d8be2362-7b68-4ea4-a1f6-03f8bc4eede7",
      "record": "7:23 minutes"
    },
    {
      "id": "365cc0bb-ba8f-41d3-bf82-83d041d38b82",
      "workout": "a24d2618-01d1-4682-9288-8de1343e53c7",
      "record": "358 reps"
    },
    {
      "id": "62251cfe-fdb6-4fa6-9a2d-c21be93ac78d",
      "workout": "4a3d9aaa-608c-49a7-a004-66305ad4ab50",
      "record": "145 reps"
    }
  ],
}
</code></pre>
<p>为了确保同一 id 下的训练相同，我也复制了一些训练到 workouts 中：</p>
<pre><code class="language-json">{
  "workouts": [
    {
      "id": "61dbae02-c147-4e28-863c-db7bd402b2d6",
      "name": "Tommy V",
      "mode": "For Time",
      "equipment": [
        "barbell",
        "rope"
      ],
      "exercises": [
        "21 thrusters",
        "12 rope climbs, 15 ft",
        "15 thrusters",
        "9 rope climbs, 15 ft",
        "9 thrusters",
        "6 rope climbs, 15 ft"
      ],
      "createdAt": "4/20/2022, 2:21:56 PM",
      "updatedAt": "4/20/2022, 2:21:56 PM",
      "trainerTips": [
        "Split the 21 thrusters as needed",
        "Try to do the 9 and 6 thrusters unbroken",
        "RX Weights: 115lb/75lb"
      ]
    },
    {
      "id": "4a3d9aaa-608c-49a7-a004-66305ad4ab50",
      "name": "Dead Push-Ups",
      "mode": "AMRAP 10",
      "equipment": [
        "barbell"
      ],
      "exercises": [
        "15 deadlifts",
        "15 hand-release push-ups"
      ],
      "createdAt": "1/25/2022, 1:15:44 PM",
      "updatedAt": "3/10/2022, 8:21:56 AM",
      "trainerTips": [
        "Deadlifts are meant to be light and fast",
        "Try to aim for unbroken sets",
        "RX Weights: 135lb/95lb"
      ]
    },
    {
      "id": "d8be2362-7b68-4ea4-a1f6-03f8bc4eede7",
      "name": "Heavy DT",
      "mode": "5 Rounds For Time",
      "equipment": [
        "barbell",
        "rope"
      ],
      "exercises": [
        "12 deadlifts",
        "9 hang power cleans",
        "6 push jerks"
      ],
      "createdAt": "11/20/2021, 5:39:07 PM",
      "updatedAt": "4/22/2022, 5:49:18 PM",
      "trainerTips": [
        "Aim for unbroken push jerks",
        "The first three rounds might feel terrible, but stick to it",
        "RX Weights: 205lb/145lb"
      ]
    },
    {
      "name": "Core Buster",
      "mode": "AMRAP 20",
      "equipment": [
        "rack",
        "barbell",
        "abmat"
      ],
      "exercises": [
        "15 toes to bars",
        "10 thrusters",
        "30 abmat sit-ups"
      ],
      "trainerTips": [
        "Split your toes to bars in two sets maximum",
        "Go unbroken on the thrusters",
        "Take the abmat sit-ups as a chance to normalize your breath"
      ],
      "id": "a24d2618-01d1-4682-9288-8de1343e53c7",
      "createdAt": "4/22/2022, 5:50:17 PM",
      "updatedAt": "4/22/2022, 5:50:17 PM"
    }
  ],
  "members": [ ...
  ],
  "records": [ ...
  ]
}
</code></pre>
<p>让我们花点时间来想想如何实现。</p>
<p>我们有一组叫做 “workouts” 的资源，还有另一组叫做 “records” 的资源。</p>
<p>在创建交叉内容的结构之前，建议先创建另一个控制器、服务层和数据组合方法来负责训练记录。</p>
<p>我们很有可能需要为训练记录实现 CRUD 端点，因为在未来我们也会添加、更新和删除记录。但这不是现在的首要任务。</p>
<p>我们也需要一个记录的路由来捕获对应的请求。这是你练习自己实现 CRUD 的绝好机会。</p>
<pre><code class="language-bash"># 创建记录控制器
touch src/controllers/recordController.js 

# 创建记录服务层
touch src/services/recordService.js 

# 创建记录数据处理方法 
touch src/database/Record.js
</code></pre>
<p>很简单！让我们从后往前，从实现数据方法开始编写。</p>
<pre><code class="language-javascript">// 在src/database/Record.js
const DB = require("./db.json");

const getRecordForWorkout = (workoutId) =&gt; {
  try {
    const record = DB.records.filter((record) =&gt; record.workout === workoutId);
    if (!record) {
      throw {
        status: 400,
        message: `Can't find workout with the id '${workoutId}'`,
      };
    }
    return record;
  } catch (error) {
    throw { status: error?.status || 500, message: error?.message || error };
  }
};
module.exports = { getRecordForWorkout };
</code></pre>
<p>很直接对不对，我们通过查询参数过滤出和训练 id 相关的记录数据</p>
<p>接下来是记录的服务层：</p>
<pre><code class="language-javascript">// 在src/services/recordService.js
const Record = require("../database/Record");

const getRecordForWorkout = (workoutId) =&gt; {
  try {
    const record = Record.getRecordForWorkout(workoutId);
    return record;
  } catch (error) {
    throw error;
  }
};
module.exports = { getRecordForWorkout };
</code></pre>
<p>这里也没有新的知识点。</p>
<p>现在就可以在训练路由创建新的路由，并且导向记录服务请求。</p>
<pre><code class="language-javascript">// 在src/v1/routes/workoutRoutes.js
const express = require("express");
const workoutController = require("../../controllers/workoutController");
// *** 添加 ***
const recordController = require("../../controllers/recordController");

const router = express.Router();

router.get("/", workoutController.getAllWorkouts);

router.get("/:workoutId", workoutController.getOneWorkout);

// *** 添加 ***
router.get("/:workoutId/records", recordController.getRecordForWorkout);

router.post("/", workoutController.createNewWorkout);

router.patch("/:workoutId", workoutController.updateOneWorkout);

router.delete("/:workoutId", workoutController.deleteOneWorkout);

module.exports = router;
</code></pre>
<p>真棒！让我们在浏览器中测试一下。</p>
<p>首先我们抓取所有训练记录，来获得一个训练 id。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/Bildschirmfoto-2022-04-30-um-15.36.48.png" alt="Bildschirmfoto-2022-04-30-um-15.36.48" width="600" height="400" loading="lazy"></p>
<p>让我们来看看能不能获得这个 id 下的所有记录。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/Bildschirmfoto-2022-04-30-um-15.36.32.png" alt="Bildschirmfoto-2022-04-30-um-15.36.32" width="600" height="400" loading="lazy"></p>
<p>如你所见，逻辑嵌套可以使资源捆绑在一起。理论上你可以想嵌套多少层就嵌套多少层，但建议至多使用三层嵌套。</p>
<p>如果你想嵌套得更深，可以稍微调整一下数据库的记录。我给你看一个小例子。</p>
<p>想象一下，前端还需要一个端点来获取到底是哪个会员持有当前记录的信息，并希望接受这个会员的所有原始信息。</p>
<p>你当然可以使用下面的 URI：</p>
<pre><code class="language-javascript">GET /api/v1/workouts/:workoutId/records/members/:memberId
</code></pre>
<p>嵌套越多，端点就越不容易管理。因此，将接受会员信息的 URI 直接存储在记录中是一个好的做法。</p>
<p>可以这样修改数据库：</p>
<pre><code class="language-json">{
  "workouts": [ ...
  ],
  "members": [ ...
  ],
  "records": [ ... {
      "id": "ad75d475-ac57-44f4-a02a-8f6def58ff56",
      "workout": "4a3d9aaa-608c-49a7-a004-66305ad4ab50",
      "record": "160 reps",
      "memberId": "11817fb1-03a1-4b4a-8d27-854ac893cf41",
      "member": "/members/:memberId"
    },
  ]
}
</code></pre>
<p>我们在数据库中添加了 “memberId” 和 “member” 这两个属性，这样我们就不需要在端点嵌套得更深。</p>
<p>前端只需要调用 <strong>GET /api/v1/workouts/:workoutId/records</strong> 便可以获得所有和训练相关的数据。</p>
<p>除此之外，我们可以由会员 id 来获取会员的信息，就可以避免更深入的嵌套。</p>
<p>当然，这一切实现的前提是处理 “/members/:memberId” 请求。😁 这听上去是锻炼你自己实现能力的好机会！</p>
<h3 id="integrate-filtering-sorting-pagination">集成过滤、排序和分页功能</h3>
<p>现在我们的 API 已经可以完成很多工作，取得了相当大的进展，但是这还不够。</p>
<p>在上一部分我们聚焦在如何提高开发者的体验，以及我们的 API 如何交互。但是 API 的整体性能也是一个关键部分，需要我们努力提高。</p>
<p>这就是为什么在我的待办清单中集成过滤、排序和分页功能也是非常关键的。</p>
<p>假设我们的 DB 中有 2000 个训练，450 条记录和 500 个会员。当我们调用端点来获取训练的时候，我们不希望一次性获得所有 2000 个训练。这样的响应速度会比较慢，导致系统崩溃（崩溃可能需要 200000 条记录😁 ）。</p>
<p>这就是为什么过滤和分页十分重要。过滤正如这个名称一样，可以帮助我们在整个数据集中获取我们需要的数据。例如所有具备“时间”模式的训练。</p>
<p>分页是另一种可以拆分数据集的机制，比方说我们可以把数据分成每页二十个训练的“页面”。这个技术确保我们一次返回不超过 20 个训练。</p>
<p>排序可以变得非常复杂，所以直接在我们的 API 排序后，再向用户发送数据更高效。</p>
<p>我们首先在 API 中整合一些过滤机制。我们将发送所有训练的这个端点升级，让这个端点接受过滤参数。通常在 GET 请求中，我们使用查询参数来添加过滤条件。</p>
<p>当我们只获取训练状态（mode）为 “AMRAP”（尽可能多地训练 <strong>A</strong>s <strong>M</strong>any <strong>R</strong>ounds <strong>A</strong>s <strong>P</strong>ossible）时，我们新的 URI 会是这样：<strong>/api/v1/workouts?mode=amrap</strong>。</p>
<p>为了让实现更有趣，我们可以添加更多的训练。请在 db.json 中的 “workouts” 数据集中添加以下代码：</p>
<pre><code class="language-json">{
  "name": "Jumping (Not) Made Easy",
  "mode": "AMRAP 12",
  "equipment": [
    "jump rope"
  ],
  "exercises": [
    "10 burpees",
    "25 double-unders"
  ],
  "trainerTips": [
    "Scale to do 50 single-unders, if double-unders are too difficult"
  ],
  "id": "8f8318f8-b869-4e9d-bb78-88010193563a",
  "createdAt": "4/25/2022, 2:45:28 PM",
  "updatedAt": "4/25/2022, 2:45:28 PM"
},
{
  "name": "Burpee Meters",
  "mode": "3 Rounds For Time",
  "equipment": [
    "Row Erg"
  ],
  "exercises": [
    "Row 500 meters",
    "21 burpees",
    "Run 400 meters",
    "Rest 3 minutes"
  ],
  "trainerTips": [
    "Go hard",
    "Note your time after the first run",
    "Try to hold your pace"
  ],
  "id": "0a5948af-5185-4266-8c4b-818889657e9d",
  "createdAt": "4/25/2022, 2:48:53 PM",
  "updatedAt": "4/25/2022, 2:48:53 PM"
},
{
  "name": "Dumbbell Rower",
  "mode": "AMRAP 15",
  "equipment": [
    "Dumbbell"
  ],
  "exercises": [
    "15 dumbbell rows, left arm",
    "15 dumbbell rows, right arm",
    "50-ft handstand walk"
  ],
  "trainerTips": [
    "RX weights for women: 35-lb",
    "RX weights for men: 50-lb"
  ],
  "id": "3dc53bc8-27b8-4773-b85d-89f0a354d437",
  "createdAt": "4/25/2022, 2:56:03 PM",
  "updatedAt": "4/25/2022, 2:56:03 PM"
}
</code></pre>
<p>当我们处理好接受和处理查询参数后，就可以编写训练的控制层：</p>
<pre><code class="language-javascript">// 在src/controllers/workoutController.js
...

const getAllWorkouts = (req, res) =&gt; {
  // *** 添加 ***
  const { mode } = req.query;
  try {
    // *** 添加 ***
    const allWorkouts = workoutService.getAllWorkouts({ mode });
    res.send({ status: "OK", data: allWorkouts });
  } catch (error) {
    res
      .status(error?.status || 500)
      .send({ status: "FAILED", data: { error: error?.message || error } });
  }
};

...
</code></pre>
<p>我们在 req.query 对象中提取 “mode”，并用作 workoutService.getAllWorkouts 的参数。这个对象包含了所有过滤参数。</p>
<p>这里我使用了简写语法，来创建一个名为 “mode” 的新键，这个键位于对象内部，其值可以是任意 “req.query.mode” 的值。可以为一个真值或者如果没有一个参数为 “mode” 的参数则为 undefined。我们可以在对象内扩充更多过滤参数。</p>
<p>在workoutService中传入数据处理方法：</p>
<pre><code class="language-javascript">// 在src/services/workoutService.js
...

const getAllWorkouts = (filterParams) =&gt; {
  try {
    // *** 添加 ***
    const allWorkouts = Workout.getAllWorkouts(filterParams);
    return allWorkouts;
  } catch (error) {
    throw error;
  }
};

...
</code></pre>
<p>现在我们可以使用数据库方法，并且应用过滤：</p>
<pre><code class="language-javascript">// 在src/database/Workout.js
...

const getAllWorkouts = (filterParams) =&gt; {
  try {
    let workouts = DB.workouts;
    if (filterParams.mode) {
      return DB.workouts.filter((workout) =&gt;
        workout.mode.toLowerCase().includes(filterParams.mode)
      );
    }
    // 如果有其他的参数，可以在这里编写其他的 if 表达式
    return workouts;
  } catch (error) {
    throw { status: 500, message: error };
  }
};

...
</code></pre>
<p>简单明了！我们在这里做的工作就是检查 “filterParams” 中是否存在键 “mode” 的真值，如果存在，则过滤出所有包含同样 “mode” 的训练，如果不存在，则返回所有训练，</p>
<p>此处我们使用 “let” 来定义 “workouts” 变量是因为如果我们使用 if 表达式来添加更多过滤器的话，会覆盖掉 “workouts” 并且串联过滤器。</p>
<p>在浏览器中可以登陆 3000/api/v1/workouts?mode=amrap，会接受到所有包含 “AMRAP” 的训练：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/Bildschirmfoto-2022-04-30-um-15.48.57.png" alt="Bildschirmfoto-2022-04-30-um-15.48.57" width="600" height="400" loading="lazy"></p>
<p>如果不填写查询参数的话，就会重新获得所有训练。你可以尝试添加 “for%20time” 作为 “mode” 的参数（记住：“%20” 代表“空格”）， 你就会获得所有包含 “For Time” 的训练，</p>
<p>如果输入一个不存在的值，则会接受到空数组。</p>
<p>排序和分页的参数页遵行同样的原理，我们来看看我们需要实现的一些功能：</p>
<ul>
<li>接受所有需要杠铃的训练：<strong>/api/v1/workouts?equipment=barbell</strong></li>
<li>接受 5 组训练：<strong>/api/v1/workouts?length=5</strong></li>
<li>使用分页时，返回第二页：<strong>/api/v1/workouts?page=2</strong></li>
<li>给训练排序，并且以创建时间为标准降序来响应训练：<strong>/api/v1/workouts?sort=-createdAt</strong></li>
<li>你也可以合并参数，获取最近更新的 10 个训练：<strong>/api/v1/workouts?sort=-updatedAt&amp;length=10</strong></li>
</ul>
<h3 id="use-data-caching-for-performance-improvements">使用数据缓存提升性能</h3>
<p>使用数据缓存也是一个提升 API 整体使用体验和性能的优秀实践。</p>
<p>当一段数据经常被请求，或者这个数据太大了需要比较长的时间加载的时候，可以使用缓存来提供数据。</p>
<p>你可以将这些数据存储到缓存，这样就可以避免每一次都重新提交数据请求。</p>
<p>但必须记住的是，使用缓存来提供数据的话，这段数据很有可能过期。所以必须确保缓存中的数据保持更新。</p>
<p>有各种实现来实现缓存的方式，一种是使用 <a href="https://www.npmjs.com/package/redis">redis</a> 或者 express 的中间件 <a href="https://www.npmjs.com/package/apicache">apicache</a>。</p>
<p>我准备使用 apicache，但如果你想使用 Redis，我强烈推荐你阅读他们的<a href="https://docs.redis.com/latest/rs/references/client_references/client_nodejs/">文档</a>。</p>
<p>我们思考一下在 API 中使用缓存的场景。我认为使用缓存来返回所有训练会更加有效。</p>
<p>首先，让我们安装中间件：</p>
<pre><code class="language-bash">npm i apicache
</code></pre>
<p>我们在训练的路由中引用这个插件并配置好：</p>
<pre><code class="language-javascript">// 在src/v1/routes/workoutRoutes.js
const express = require("express");
// *** 添加 ***
const apicache = require("apicache");
const workoutController = require("../../controllers/workoutController");
const recordController = require("../../controllers/recordController");

const router = express.Router();
// *** 添加 ***
const cache = apicache.middleware;

// *** 添加 ***
router.get("/", cache("2 minutes"), workoutController.getAllWorkouts);

router.get("/:workoutId", workoutController.getOneWorkout);

router.get("/:workoutId/records", recordController.getRecordForWorkout);

router.post("/", workoutController.createNewWorkout);

router.patch("/:workoutId", workoutController.updateOneWorkout);

router.delete("/:workoutId", workoutController.deleteOneWorkout);

module.exports = router;
</code></pre>
<p>很简单！我们可以将新的缓存命名为 <strong>apicache.middleware</strong>，并在路由中当作中间件来使用。仅需在实际的路径和训练控制器之间放置这个参数。</p>
<p>你可以在中间件内部定义你需要保存缓存多久。在这篇教程中我选择 2 分钟。保存时间一般取决于你存储的数据多久更新一次。</p>
<p>让我们测试一下！</p>
<p>在 Postman 或者另外的 HTTP 客户端中，定义一个新的请求，获取所有的训练。之前我都是在浏览器中操作，但是这次我想给你更直观的感受，所以使用 Postman。</p>
<p>让我们第一次请求数据：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/Bildschirmfoto-2022-04-26-um-15.36.46-1.png" alt="Bildschirmfoto-2022-04-26-um-15.36.46-1" width="600" height="400" loading="lazy"></p>
<p>你可以看到我们的 API 花了 22.93 毫秒来响应。一旦缓存被清空（2 分钟后），又回重新抓取数据保存到缓存，我们第一次获取数据的时候就将数据存储到了缓存。</p>
<p>在上述例子中，数据并不是有由缓存提供。而是通过“普通”方式来抓去数据库保存到缓存。</p>
<p>现在我们第二次请求数据，响应时间变短，因为我们直接从缓存中返回数据。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/Bildschirmfoto-2022-04-26-um-15.36.59-1.png" alt="Bildschirmfoto-2022-04-26-um-15.36.59-1" width="600" height="400" loading="lazy"></p>
<p>比起第一次请求，我们快了三倍，这完全归功于缓存。</p>
<p>在我们的例子中，我们只缓存了一个路由，你可以在所有路由中应用：</p>
<pre><code class="language-javascript">// 在src/index.js
const express = require("express");
const bodyParser = require("body-parser");
// *** 添加 ***
const apicache = require("apicache");
const v1WorkoutRouter = require("./v1/routes/workoutRoutes");

const app = express();
// *** 添加 ***
const cache = apicache.middleware;
const PORT = process.env.PORT || 3000;

app.use(bodyParser.json());
// *** 添加 ***
app.use(cache("2 minutes"));
app.use("/api/v1/workouts", v1WorkoutRouter);

app.listen(PORT, () =&gt; {
  console.log(`API is listening on port ${PORT}`);
});
</code></pre>
<p>还有一件关于缓存的<strong>重要</strong>的事情，虽然在这个例子中，缓存给你节省了不少时间，但是缓存也可以给应用造成不小的麻烦：</p>
<p>当使用缓存时，你需要注意的事：</p>
<ul>
<li>必须保证缓存中的数据是更新的，你可不想提供过期的数据</li>
<li>当第一个请求在执行的过程中，数据被保存到缓存，也有更多地请求进来，你必须决定是延迟其他的请求，从缓存中提供数据，还是其他的请求也如第一次请求一样从数据库来获取数据</li>
<li>如果使用分布式缓存如 Redis，缓存会是你的结构中的一个组件，所以你必须考虑一下是否有必要使用缓存</li>
</ul>
<p>我常常这么做：</p>
<p>当我在搭建的时候我希望一切从简，API 同理。</p>
<p>首次搭建 API 的时候没有特别的原因使用缓存，我会等使用了一段时间之后，有理由使用缓存后再使用缓存。</p>
<h3 id="good-security-practices">好的安全实践</h3>
<p>这是一段不错的旅行，我们讲了许多 API 相关的重要观点，并且扩充了我们的 API。</p>
<p>我们已经讲了提升 API 使用和性能的最佳实践。安全也是 API 重要的一环。如果你创建出一个绝佳的 API，但是在服务器上运行的时候却十分脆弱，那这个 API 就变得无用且危险。</p>
<p>首先必须使用的是 SSL/TLS，因为这是当今互联网通讯的一个标准。特别是当 API 需要在客户端和服务器之间传输私人数据的时候。</p>
<p>如果你需要给验证客户提供数据，必须使用验证手段来保护数据。</p>
<p>在 Express 中，我们可以像在缓存中那样在路由中插入特定的中间件来检查请求的真实性再获取资源。</p>
<p>API 中的一些资源和交互是你可能不希望所有用户都可以请求的。这是就需要一个角色系统。在路由中添加一个检查逻辑来验证用户是否有权利来获取这些数据。</p>
<p>用户角色在我们的用例中也同样适用。比方说我们需要特定用户（教练）来使用创建、更新和删除训练和记录的功能。所有用户可以读取（同样可成为“普通”用户）。</p>
<p>这可以通过在路由中插入中间件来实现。如在我们的 /api/v1/workouts 的 POST 请求中插入。</p>
<p>在第一个中间件中我们检查用户是不是真实的，如果为真，就进入下一个中间件来检查用户角色，如果用户符合获取资源的角色，就移交到对应的控制器。</p>
<p>路由处理器如下：</p>
<pre><code class="language-javascript">// 在src/v1/routes/workoutRoutes.js
...

// 定制中间件
const authenticate = require("../../middlewares/authenticate");
const authorize = require("../../middlewares/authorize");

router.post("/", authenticate, authorize, workoutController.createNewWorkout);

...
</code></pre>
<p>这个话题相关的最佳实践，我推荐阅读<a href="https://restfulapi.net/security-essentials/">这篇文章</a>。</p>
<h3 id="document-your-api-properly">给 API 编写合适的文档</h3>
<p>对于开发者来说编写文档确实不是一件让他们乐意干的活儿，但是是必须要做的事，特别是API的文档。</p>
<p>有人说过：</p>
<blockquote>
<p>“API 得和文档一样优秀”</p>
</blockquote>
<p>我认为这句话挺有道理，因为如果 API 的文档不好，这个 API 就不好使用。文档帮助开发者更方便地使用 API。</p>
<p>永远记住文档是 API 使用者和 API 交互的第一环节。用户能够更快读懂文档，就能够更快使用 API。</p>
<p>所以我们必须编写良好精确的文档。有一些比较好用的工具可以帮助我们实现。</p>
<p>和其他计算机科学领域一样，API 文档也有标准，查看 <a href="https://swagger.io/specification/">OpenAPI 细则</a>。</p>
<p>让我们来看看如何遵循这份规则来创建文档。 我们将使用 <a href="https://www.npmjs.com/package/swagger-ui-express">swagger-ui-express</a> 和 <a href="https://www.npmjs.com/package/swagger-jsdoc">swagger-jsdoc</a> 工具包。你马上就会为这两个工具包能够做到的事感到惊奇。</p>
<p>首先我们设置好文档的基础结构。因为我们会有不同版本的 API，所以文档会有些许不同，这就是为什么我会创建 swagger 文件，来处理不同版本的文档。</p>
<pre><code class="language-bash"># 安装必须的 NPM 包
npm i swagger-jsdoc swagger-ui-express 

# 创建新的文件来设置 swagger 文档
touch src/v1/swagger.js
</code></pre>
<pre><code class="language-javascript">// 在 src/v1/swagger.js 中
const swaggerJSDoc = require("swagger-jsdoc");
const swaggerUi = require("swagger-ui-express");

// API的基础信息
const options = {
  definition: {
    openapi: "3.0.0",
    info: { title: "Crossfit WOD API", version: "1.0.0" },
  },
  apis: ["./src/v1/routes/workoutRoutes.js", "./src/database/Workout.js"],
};

// 使用 JSON 格式的文档
const swaggerSpec = swaggerJSDoc(options);

// 设置文档的函数
const swaggerDocs = (app, port) =&gt; {
  // 处理文档路由
  app.use("/api/v1/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
  // 使得允许使用 JSON 格式文档
  app.get("/api/v1/docs.json", (req, res) =&gt; {
    res.setHeader("Content-Type", "application/json");
    res.send(swaggerSpec);
  });
  console.log(
    `Version 1 Docs are available on http://localhost:${port}/api/v1/docs`
  );
};

module.exports = { swaggerDocs };
</code></pre>
<p>设置很简单，我们定义了 API 的基本数据，创建了 JSON 格式的文档，并创建了函数使文档可用。</p>
<p>为了检查一切可以运行，我们在控制台打印一个简单的信息。</p>
<p>这是我们在根文件中会使用到的函数，在根文件中我们也创建了 Express 服务器，确保文档也被启动。</p>
<pre><code class="language-javascript">// 在src/index.js
const express = require("express");
const bodyParser = require("body-parser");
const v1WorkoutRouter = require("./v1/routes/workoutRoutes");
// *** 添加 ***
const { swaggerDocs: V1SwaggerDocs } = require("./v1/swagger");

const app = express();
const PORT = process.env.PORT || 3000;

app.use(bodyParser.json());
app.use("/api/v1/workouts", v1WorkoutRouter);

app.listen(PORT, () =&gt; {
  console.log(`API is listening on port ${PORT}`);
  /// *** 添加 ***
  V1SwaggerDocs(app, PORT);
});
</code></pre>
<p>现在你可以在你的控制台查看服务器是否在运行。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/Bildschirmfoto-2022-04-28-um-20.23.51-1.png" alt="Bildschirmfoto-2022-04-28-um-20.23.51-1" width="600" height="400" loading="lazy"></p>
<p>当你登陆 localhost:3000/api/v1/docs，你会看到我们的文档已经准备好了：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/Bildschirmfoto-2022-04-28-um-20.25.00-1.png" alt="Bildschirmfoto-2022-04-28-um-20.25.00-1" width="600" height="400" loading="lazy"></p>
<p>每次我都会感叹运作得如此顺畅。现在基本结构已经设置好，我们可以来实现文档的端点了，让我们开始吧！</p>
<p>当你查看 swagger.js 文件中的 <strong>options.apis</strong>，你会发现我们已经预留了处理训练路由和数据库中训练文件的路径。 这就是让魔法实现最重要的环节。</p>
<p>在 swagger 中有这些选项使得我们可以使用评论来引用 OpenAPI，并且使用类似 yaml 的语法来编写文档，这就是设置文档的所有必须条件了。</p>
<p>现在我们就可以开始来创建我们文档的第一个端点了，让我们开始吧！</p>
<pre><code class="language-javascript">// 在src/v1/routes/workoutRoutes.js
...

/**
 * @openapi
 * /api/v1/workouts:
 *   get:
 *     tags:
 *       - Workouts
 *     responses:
 *       200:
 *         description: OK
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 status:
 *                   type: string
 *                   example: OK
 *                 data:
 *                   type: array 
 *                   items: 
 *                     type: object
 */
router.get("/", cache("2 minutes"), workoutController.getAllWorkouts);

...
</code></pre>
<p>这基本上就是使用 swagger 文档来添加端点的所有魔法了，你可以在他们的<a href="https://swagger.io/docs/specification/about/">文档</a>中查看所有细则。</p>
<p>当你重新加载文档页面，你会看到如下：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/Bildschirmfoto-2022-04-29-um-07.21.51-1.png" alt="Bildschirmfoto-2022-04-29-um-07.21.51-1" width="600" height="400" loading="lazy"></p>
<p>如果你熟悉 OpenAPI 文档的话，这个画面对于你来说就不陌生。在这个页面中我们会看到所有端点，并且包含每一个端点的信息。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/Bildschirmfoto-2022-04-29-um-07.41.46-1.png" alt="Bildschirmfoto-2022-04-29-um-07.41.46-1" width="600" height="400" loading="lazy"></p>
<p>但你仔细看会发现我们还没有定义正确的返回值，因为我们的 “data” 属性仅设定为一个空对象。</p>
<p>这时模式（schema）就发挥了作用。</p>
<pre><code class="language-javascript">// 在src/databse/Workout.js
...

/**
 * @openapi
 * components:
 *   schemas:
 *     Workout:
 *       type: object
 *       properties:
 *         id: 
 *           type: string
 *           example: 61dbae02-c147-4e28-863c-db7bd402b2d6
 *         name: 
 *           type: string
 *           example: Tommy V  
 *         mode:
 *           type: string
 *           example: For Time
 *         equipment:
 *           type: array
 *           items:
 *             type: string
 *           example: ["barbell", "rope"]
 *         exercises:
 *           type: array
 *           items:
 *             type: string
 *           example: ["21 thrusters", "12 rope climbs, 15 ft", "15 thrusters", "9 rope climbs, 15 ft", "9 thrusters", "6 rope climbs, 15 ft"]
 *         createdAt:
 *           type: string
 *           example: 4/20/2022, 2:21:56 PM
 *         updatedAt: 
 *           type: string
 *           example: 4/20/2022, 2:21:56 PM
 *         trainerTips:
 *           type: array
 *           items:
 *             type: string
 *           example: ["Split the 21 thrusters as needed", "Try to do the 9 and 6 thrusters unbroken", "RX Weights: 115lb/75lb"]
 */

...
</code></pre>
<p>在上面的示例中，我们创建了第一个模式，通常在你的模式或者模型文件（model file）中的定义的是数据模型。</p>
<p>这也很简单明了。我们定义了所有训练的属性，包括种类和例子。</p>
<p>再次浏览文档页面，你会看到另一个由模式主导的板块。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/Bildschirmfoto-2022-04-29-um-07.29.49-1.png" alt="Bildschirmfoto-2022-04-29-um-07.29.49-1" width="600" height="400" loading="lazy"></p>
<p>我们可以在端点的响应中引用这个模式。</p>
<pre><code class="language-javascript">// 在src/v1/routes/workoutRoutes.js
...

/**
 * @openapi
 * /api/v1/workouts:
 *   get:
 *     tags:
 *       - Workouts
 *     responses:
 *       200:
 *         description: OK
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 status:
 *                   type: string
 *                   example: OK
 *                 data:
 *                   type: array 
 *                   items: 
 *                     $ref: "#/components/schemas/Workout"
 */
router.get("/", cache("2 minutes"), workoutController.getAllWorkouts);

...
</code></pre>
<p>仔细看最底部的评论，在 “item” 内部，我们使用了 “$ref” 来创建引用，引用我们在训练文件中定义的模式。</p>
<p>现在我们就可以完整地展示训练的响应了。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/Bildschirmfoto-2022-04-29-um-07.44.12-1.png" alt="Bildschirmfoto-2022-04-29-um-07.44.12-1" width="600" height="400" loading="lazy"></p>
<p>很不错！你可能会认为“编写这些评论很繁琐”。</p>
<p>这或许是真的，但是可以这样想，写在代码中的评论里可以帮助你作为一个 API 开发者更好地了解你的 API。当你想要了解某一个端点的时候，你不要阅读所有文档，你可以直接在代码中找到。</p>
<p>为端点写文档也可以帮助你更好的了解这些端点，“逼迫”自己去思考在实现的过程当中缺失了什么。</p>
<p>你看我确实忘记了一些东西，我忘记写可能出现的错误响应，查询参数也缺少了。</p>
<p>让我们调整一下：</p>
<pre><code class="language-javascript">// 在src/v1/routes/workoutRoutes.js
...

/**
 * @openapi
 * /api/v1/workouts:
 *   get:
 *     tags:
 *       - Workouts
 *     parameters:
 *       - in: query
 *         name: mode
 *         schema:
 *           type: string
 *         description: The mode of a workout
 *     responses:
 *       200:
 *         description: OK
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 status:
 *                   type: string
 *                   example: OK
 *                 data:
 *                   type: array 
 *                   items: 
 *                     $ref: "#/components/schemas/Workout"
 *       5XX:
 *         description: FAILED
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 status: 
 *                   type: string
 *                   example: FAILED
 *                 data:
 *                   type: object
 *                   properties:
 *                     error:
 *                       type: string 
 *                       example: "Some error message"
 */
router.get("/", cache("2 minutes"),  workoutController.getAllWorkouts);

...
</code></pre>
<p>当你查看评论最上方 “tag” 内部，会发现我添加了另一个键叫做 “parameters”，这里可以定义我们过滤所需的查询参数。</p>
<p>我们的文档也会展示出来：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/Bildschirmfoto-2022-04-29-um-08.03.00-1.png" alt="Bildschirmfoto-2022-04-29-um-08.03.00-1" width="600" height="400" loading="lazy"></p>
<p>现阶段展示可能出现的错误，我们只用抛出 5XX 报错就行。所以在 “responses” 内部，我们可以定义另一个文档。</p>
<p>现在我们的文档页面如下：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/Bildschirmfoto-2022-04-29-um-08.04.44-2.png" alt="Bildschirmfoto-2022-04-29-um-08.04.44-2" width="600" height="400" loading="lazy"></p>
<p>很棒！我们已经给一个端点创建了完整的文档，我强烈建议你为剩下的端点创建对应的文档，在这个过程中你会学到很多东西。</p>
<p>你或许也体会到了为 API 写文档并不总是一件头疼的事，使用我介绍给你的工具可以减轻你不少负担，搭建过程也十分简单明了。</p>
<p>这样你就可以把注意力集中在重要的事情上，编写文档内容。swagger 和 OpenAPI 的文档非常不错，在网络上你也可以找到其他的优秀例子。</p>
<p>那么因为太多“额外”工作而不写文档这个理由现在就不成立了。</p>
<h2 id="conclusion">总结</h2>
<p>呼！这是一趟有趣的旅程！我非常享受写这篇文章也从中学习了很多。</p>
<p>这些最佳实践中有一些可能很重要。另一些可能不适用于你现在的情况。没关系，正如我一开始说的那样，对于开发者来说最重要的是能够根据情况挑选出最适合自己的方法。</p>
<p>我尽力把所有最佳实践融汇到这个 API 项目中，我从中获得非常多的乐趣。</p>
<p>我十分乐意接受各种反馈，任何你想要告诉我的事情（好的或坏的），别迟疑，请告诉我：</p>
<p>这是我的<a href="https://www.instagram.com/jean_marc.dev/">Instagram</a>（你也可以关注我在软件工程师的成长道路上的见闻）。</p>
<p>下篇文章见！</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何使用 REST API ]]>
                </title>
                <description>
                    <![CDATA[ 原文：How to Use REST APIs – A Complete Beginner's Guide [https://www.freecodecamp.org/news/how-to-use-rest-api/]，作者：Alex Husar [https://www.freecodecamp.org/news/author/alex-husar/] 应用程序编程接口（API）是一个需要掌握的重要编程概念。如果花时间学习这些接口，你就可以更轻松地管理任务。 最常见的API之一就是REST API。如果你曾经尝试过从另一个网站（如：Twitter或Github）上获取数据，你可能已经使用过这个API。 那为什么理解REST API对你有帮助？REST API是如何确保现代业务的连通性的？ 在创建或者运行一个API（这里特指REST API）之前，你得先学习什么是API。这篇文章会带着你一步一步理解REST API的基本原则，以及这些原则如何使得REST API成为强大的应用。 API是如何工作的，我们为什么需要它？ API代表一组定义和协议。你需要使用API来开发和集成应 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-use-rest-api/</link>
                <guid isPermaLink="false">626ba0d9395ec5063718afac</guid>
                
                    <category>
                        <![CDATA[ RESTful API ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ PapayaHUANG ]]>
                </dc:creator>
                <pubDate>Fri, 29 Apr 2022 02:23:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/04/The-Complete-Guide-to-Understanding-and-Using-REST-APIs-1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/how-to-use-rest-api/">How to Use REST APIs – A Complete Beginner's Guide</a>，作者：<a href="https://www.freecodecamp.org/news/author/alex-husar/">Alex Husar</a></p><!--kg-card-begin: markdown--><p>应用程序编程接口（API）是一个需要掌握的重要编程概念。如果花时间学习这些接口，你就可以更轻松地管理任务。</p>
<p>最常见的API之一就是REST API。如果你曾经尝试过从另一个网站（如：Twitter或Github）上获取数据，你可能已经使用过这个API。</p>
<p>那为什么理解REST API对你有帮助？REST API是如何确保现代业务的连通性的？</p>
<p>在创建或者运行一个API（这里特指REST API）之前，你得先学习什么是API。这篇文章会带着你一步一步理解REST API的基本原则，以及这些原则如何使得REST API成为强大的应用。</p>
<h2 id="api"><strong>API是如何工作的，我们为什么需要它？</strong></h2>
<p>API代表一组定义和协议。你需要使用API来开发和集成应用，因为API可以协调两个软件之间的数据交换，如信息供应商（服务器）和用户之间。</p>
<p>客户向生产商发出调用，生产商返回响应。API规定了返回的可访问内容。</p>
<p>程序使用API进行通信、检索信息或执行功能。API允许用户通过这一系统返回想要的结果。</p>
<p>简言之，API就是用户（客户）和资源（服务器）之间的中间人。</p>
<p>当用户发出API请求或者浏览一个在线商城，会期望快速得到反馈，所以作为开发者你需要优化加载时间（参考——<a href="https://onilab.com/blog/magento-ttfb-optimization/">优化麦进斗TTFB (Time To First Byte)</a>）或者使用其他更适合你的内容管理系统（CMS）的性能提升策略。</p>
<p>集成API的原因包括：</p>
<ul>
<li>精简资源、信息共享</li>
<li>通过<a href="https://www.freecodecamp.org/news/authenticate-and-authorize-apis-in-dotnet5/">验证和定义权利</a>的手段来控制特定人群访问特定内容</li>
<li>安全性和控制权</li>
<li>无需了解软件细节</li>
<li>即便使用不同的技术，服务间也可以保持持续通信</li>
</ul>
<h2 id="restapi"><strong>REST API概览</strong></h2>
<p><img src="https://lh3.googleusercontent.com/DUwmoHyRnoD1WovETSrQdSaIv8rh5WUVPxVjPN9_cvVokx7E4fZxzGyCY0_XMRA2cikjPkWIUDlXmtDqqGDX-KCzya5EVEEgxi8sEVwpVTeiHBNsqCULC-78QCE4dJ0_ieC1mQzn" alt="DUwmoHyRnoD1WovETSrQdSaIv8rh5WUVPxVjPN9_cvVokx7E4fZxzGyCY0_XMRA2cikjPkWIUDlXmtDqqGDX-KCzya5EVEEgxi8sEVwpVTeiHBNsqCULC-78QCE4dJ0_ieC1mQzn" width="600" height="400" loading="lazy"></p>
<p>RESTful指的是一种软件架构，即“表现层状态转移”（Representational State Transfer)。你可能在有关制定信息交换系统（web服务）标准的内容中听到过这个表达。</p>
<p>web服务通过无状态协议使得在线资源由文本的方式呈现，并且可以读取和处理这些在线资源。客户端通过著名的HTTP协议进行数据获取、更新和删除。</p>
<p>REST于2000年被首次提出，目的是通过对API采取特定的规范来提升API的性能、可扩展性并简化API。</p>
<p>由于可以广泛兼容各种设备和应用，REST API变得越来越受欢迎。下图列举了使用REST API的一些场景：</p>
<p><img src="https://lh4.googleusercontent.com/Jk2xFwUgtgRzOuJuSa9kiWPPe51CN0qLd2hXMJ3F2SyW6MM10Gzq2qIY36dDQQj6fPJPG7Axl3q431QumWwi3WtYyFC1FA5TcI1i7i5PeQOO38tpdSCgIF0dJktnVhoWvVjAwFOK" alt="Jk2xFwUgtgRzOuJuSa9kiWPPe51CN0qLd2hXMJ3F2SyW6MM10Gzq2qIY36dDQQj6fPJPG7Axl3q431QumWwi3WtYyFC1FA5TcI1i7i5PeQOO38tpdSCgIF0dJktnVhoWvVjAwFOK" width="600" height="400" loading="lazy"></p>
<h3 id="1web">1. Web使用</h3>
<p>REST并没有限制客户端技术，因此适用于各种各样的项目，如：</p>
<ul>
<li>web开发</li>
<li>iOS应用</li>
<li>IoT设备</li>
<li>Windows手机应用</li>
</ul>
<p>因为不必拘泥于某一种客户端技术栈，所以你可以使用REST为公司搭建任意基础设施。</p>
<h3 id="2">2. 云应用</h3>
<p>由于无状态特性，调用REST API对于云应用来说是理想的解决方案。一旦出现问题，你可以重新部署无状态组件，组件会管理流量转移。</p>
<h3 id="3">3. 云计算</h3>
<p>与服务连接的API需要控制URL的解码方式，所以REST在云服务中作用巨大。</p>
<p>云计算和微服务的发展使得RESTful API架构在将来会成为一种常态。</p>
<h2 id="restapi">REST API是如何运作的?</h2>
<p>数据（如：图像、视频和文本）实体化了REST的资源。客户浏览一个特定的URL，并向服务器发送请求以获得响应。</p>
<p><img src="https://lh4.googleusercontent.com/HwYHNtAz8M84Tggswzk662nm_dyGUA77st12KGsiqw4rVBGqhJM2gQ5wgL2sL8ZhWmwOGsoEJx6Uqt7TdxU4Bkbg_uccr2UVTXtWsxnR495yZReGoY_reZEd9rq5_9vnjiaUUBs2" alt="HwYHNtAz8M84Tggswzk662nm_dyGUA77st12KGsiqw4rVBGqhJM2gQ5wgL2sL8ZhWmwOGsoEJx6Uqt7TdxU4Bkbg_uccr2UVTXtWsxnR495yZReGoY_reZEd9rq5_9vnjiaUUBs2" width="600" height="400" loading="lazy"></p>
<h3 id="restapi"><strong>REST API背后的概念</strong></h3>
<p>一个请求（你访问的URL）包含以下四个方面：</p>
<ul>
<li><strong>终点（路径）</strong>，即以 <code>root-endpoint/?</code>为结构的URL</li>
<li><strong>请求方式</strong>，有五种请求方式： GET、POST、PUT、PATCH、DELETE</li>
<li><strong>请求头</strong>，包含各种功能，如信息验证以及请求体的内容(可以使用 <code>-H</code>或<code>--header</code>来发送HTTP请求头)</li>
<li><strong>数据（请求体）</strong>，是你通过 <code>-d</code>或<code>--data</code>向服务器发送的POST, PUT, PATCH或DELETE请求。</li>
</ul>
<p>HTTP请求允许你使用以下方式处理数据，如：</p>
<ul>
<li>POST请求创建记录</li>
<li>GET请求从服务器读取或获取资源（如图像文件或者其他资源合集）</li>
<li>PUT和PATCH请求更新记录</li>
<li>DELETE请求服务器删除某个资源</li>
</ul>
<p>这四种方式可以总结为CRUD（增删查改）：建立（Create）、读取（Read）、改正（Update）和删除（Delete）。<br>
<img src="https://lh5.googleusercontent.com/Quydyrq2Zw2Mh3uJj4G9LE40DhjJyWLjRCU9-hqs0uKt-hGCgoyGVP9eiU_6IBnb6GwxsILeu9kqjO5LQ6s7LBmHDtbksnqb13YtPoCKRq062zXi1Pz4wf0GAO27maHMlhamixAz" alt="Quydyrq2Zw2Mh3uJj4G9LE40DhjJyWLjRCU9-hqs0uKt-hGCgoyGVP9eiU_6IBnb6GwxsILeu9kqjO5LQ6s7LBmHDtbksnqb13YtPoCKRq062zXi1Pz4wf0GAO27maHMlhamixAz" width="600" height="400" loading="lazy"></p>
<p>服务器使用以下格式向客服端发送数据：</p>
<ul>
<li><a href="https://www.freecodecamp.org/news/html-best-practices/">HTML</a></li>
<li>JSON (由于其独立于计算机语言以及人机都可以访问的特性，这是目前最常用的格式)</li>
<li>XLT</li>
<li>PHP</li>
<li>Python</li>
<li>纯文本</li>
</ul>
<h2 id="restapi">为什么使用REST API?</h2>
<p>选择REST而不是其他的API，如SOA是出于一系列原因，比如：REST易于扩展、操作灵活、可移植性和独立性。</p>
<p><img src="https://lh4.googleusercontent.com/yJ2QDrpGbA-RpzwhXOXr1yl9aGTvVHXeiuyBvFxsMtE5KQu2wRmNLwlCX7cNGOlp1TjRK-P9VsBsFaGRNkxZw-QWvggxqXLYFtLg-THClHzB-5GJlMX6hGkY3DQnFh1YpzkHt2iE" alt="yJ2QDrpGbA-RpzwhXOXr1yl9aGTvVHXeiuyBvFxsMtE5KQu2wRmNLwlCX7cNGOlp1TjRK-P9VsBsFaGRNkxZw-QWvggxqXLYFtLg-THClHzB-5GJlMX6hGkY3DQnFh1YpzkHt2iE" width="600" height="400" loading="lazy"></p>
<h3 id="">不依赖项目架构</h3>
<p>独立运行的客户端和服务器意味着开发者不受任何项目部分的约束。由于REST API的自适应，开发者可以分别开发各个部分，互不打扰。</p>
<h3 id="">可移植性和适应性</h3>
<p>REST API仅当某个请求的数据发送成功时运作。你可以从一个服务器迁移到另一个服务器，并且随时更新数据。</p>
<h3 id="">可在未来扩展项目</h3>
<p>由于客户端和服务器相互独立，开发者可以迅速开发产品。</p>
<h2 id="restful"><strong>RESTful架构的风格特征</strong></h2>
<p>若使用SOAP、XML-RPC这类API，开发者必须构思出严谨的架构。但是REST API与众不同，REST广泛支持数据类型，也可以使用几乎任何编程语言编写。</p>
<p>六种REST架构限制是设计解决方案的原则，具体如下：</p>
<p><img src="https://lh5.googleusercontent.com/XRsmwgFoTf1sCI3hZf6n5DxHXDqHclunxf6ocqxjUVgWPss5KHiz8wm4fXYzCJ9mkijpfwhGc-YzSO_R1fm9JtOej1T1SQJwngs-wK_Lz0DhUwI2LfCOQWsZvm88nVlkGkmBgV-E" alt="XRsmwgFoTf1sCI3hZf6n5DxHXDqHclunxf6ocqxjUVgWPss5KHiz8wm4fXYzCJ9mkijpfwhGc-YzSO_R1fm9JtOej1T1SQJwngs-wK_Lz0DhUwI2LfCOQWsZvm88nVlkGkmBgV-E" width="600" height="400" loading="lazy"></p>
<h3 id="1"><strong>1. 统一接口（一致的用户接口）</strong></h3>
<p>这个概念规定不论源头是哪里，所有请求同一个资源的API必须一致，即使用同一种语言。统一标识符（URI）和关联数据一一对应，如用户名或者电子邮件地址。</p>
<p>统一接口原则的另一个要求是信息必须是自我描述的。即信息必须是服务器可以理解并决定如何处理的（如，请求类型、MIME类型等）。</p>
<h3 id="2"><strong>2. 客户端和服务器分离</strong></h3>
<p>REST架构风格采取了特殊的方式实现客户端和服务器。也就是说，客户端和服务器可以在不知道彼此的情况下实现。</p>
<p>例如，客户端仅使用统一标识符（URI）请求资源，并不能使用其他方式和服务器通信。同时，服务器无法影响客户端，仅通过HTTP协议传输必要的数据。</p>
<p>这意味着你可以随时修改客户端代码，完全不影响服务器的运行。</p>
<p>这同样适用于服务器：改变服务端的代码不会影响客户端的运行。</p>
<p>你可以保持客户端和服务器程序的模块化和独立性，只要两边都知道向对方发送什么格式的信息就行。</p>
<p>将用户接口问题和数据存储限制分离的好处是什么呢？我们提高了接口的灵活性，可以跨不同平台使用，并且提高了扩展的可能。</p>
<p>另外，每一个组件从分离受益，因为组件可以独立进化。一个REST接口可以帮助不同的客户端：</p>
<ul>
<li>访问相同的REST终点</li>
<li>执行相同的活动</li>
<li>获得相同的响应</li>
</ul>
<h3 id="3"><strong>3. 客户端和服务器之间的无状态通信</strong></h3>
<p>基于REST的系统是无状态的，意味着客户端状态对于服务器来说未知，反之亦然。这样的限制可以确保服务器和客户端之间理解每条信息，即便是上一条信息不知情的情况下。</p>
<p>为了加强对无状态的限制，你必须使用资源而非命令。资源是网络的名词。使用名词的目的是描述你想要从其他服务获取或者通信的对象。</p>
<p>你可以在不影响这个系统的情况下控制、改变以及复用组件，所以限制的好处包括：</p>
<ul>
<li>稳定性</li>
<li>速度</li>
<li>RESTful应用的可扩展性</li>
</ul>
<p>注意每一个请求必须包括你想要的所有信息，这个请求才得以完成。客户端必须保存会话状态，因为服务端不会存储和请求相关的任何数据。</p>
<h3 id="4"><strong>4. 可缓存数据</strong></h3>
<p>REST要求在可能的情况下缓存服务端和客户端的资源。网络发展到今天，数据和响应的缓存对于客户端性能提升至关重要。</p>
<p>这对用户有什么影响？一个管理良好的缓存可以减少客户端的通信。</p>
<p>缓存也增加了服务器扩展的可能性，因为这样减轻了服务器的任务压力。缓存提高了页面加载的速度，同时也使得用户可以在不需要网络连接的情况下浏览之前浏览过的内容。</p>
<h3 id="5"><strong>5. 分层系统架构</strong></h3>
<p><img src="https://lh3.googleusercontent.com/DBk2dcqnTMZdz-dBA0sFDUe5cQu71VxMqG8pW-ux4rqNvkVcsixRNR_ZyuY1z6UeWWZ5NRV11FPIv8XYK86EGr2G-Nnb7O_njC9PER6a5TdmfpZ2qmRTI7f9P--S7QU50cYwD9EC" alt="DBk2dcqnTMZdz-dBA0sFDUe5cQu71VxMqG8pW-ux4rqNvkVcsixRNR_ZyuY1z6UeWWZ5NRV11FPIv8XYK86EGr2G-Nnb7O_njC9PER6a5TdmfpZ2qmRTI7f9P--S7QU50cYwD9EC" width="600" height="400" loading="lazy"></p>
<p>RESTful分层架构也是我们要讨论的一个限制。这个原则是将特定功能的层分到一组。</p>
<p>REST API的层各司其职，并且按层次顺序排列。例如，第一层是负责从服务器存储数据的，第二层就负责从另一个服务器部署API，第三层就负责从再一个服务器验证请求。</p>
<p>这些分层像中间人一样防止服务器和客户端直接通信。所以，客户端并不知道他们的请求发送给了哪一个服务器或者组件。</p>
<p>在传输信息前每一层各司其职意味着什么？这样可以提高API整体的安全性和灵活性，因为增加、修改或者删除API都不会影响其他接口组件。</p>
<h3 id="6"><strong>6. 按需编码（非强制性）</strong></h3>
<p>使用REST API最常见的场景是传输如XML或者JSON格式的静态资源。</p>
<p>这样的架构风格方便用户以Java小程序或脚本（JavaScript）的形式来下载并运行代码。例如，客户端可以调用API来检索和渲染UI插件的代码。</p>
<h2 id="restapi"><strong>使用REST API面临的挑战</strong></h2>
<p>了解REST API的设计和架构限制后，你需要了解使用这种架构风格将迎接的问题：</p>
<p><img src="https://lh3.googleusercontent.com/FnzdrS-v1CIkyY6lWVBZymkIbLGDOQb4ZFAPqcJD6_EDL9QL1Xd3KGwd2SP24GfYO2CTwO4-9ra4a8Dc8gOvokndr3uO7Zt0-VOjQjR6bdcLrSH3SWK0vmAeg5mZlEavHkgpsIhh" alt="FnzdrS-v1CIkyY6lWVBZymkIbLGDOQb4ZFAPqcJD6_EDL9QL1Xd3KGwd2SP24GfYO2CTwO4-9ra4a8Dc8gOvokndr3uO7Zt0-VOjQjR6bdcLrSH3SWK0vmAeg5mZlEavHkgpsIhh" width="600" height="400" loading="lazy"></p>
<h3 id="rest">REST终点的一致性</h3>
<p>无论URL如何构造，API都应该保持一致。但随着可用组合方法数量的增加，保持大型代码库的一致性变得越来越难。</p>
<h3 id="restapi">REST API的特性版本</h3>
<p>API需要定期<a href="https://www.freecodecamp.org/news/how-to-version-a-rest-api/">更新或控制版本</a>以防止兼容性问题。旧版本的终点保持运行常常会增加工作量。</p>
<h3 id="">大量认证方式</h3>
<p>你可以限制特定用户访问特定资源。比方说，你可以决定哪一个第三方服务器可以访问顾客的电子邮箱地址或者其他的敏感信息，以及服务器可以对这个信息做什么。</p>
<p>但20个各不相同的认证方式会导致初始化API调用变得十分复杂。这一初始化难题使得开发者不愿意推进项目进展。</p>
<h3 id="restapi">REST API的安全弱点</h3>
<p>尽管RESTful API是分层结构，仍存在安全隐患。例如一个应用因为缺乏加密而不够安全，就会泄露敏感数据。</p>
<p>又比如黑客每秒发送成千上万的API请求，导致DDoS攻击，或者采用其他滥用API服务的行为，使服务器崩溃。</p>
<h3 id="">过量的数据收集和请求</h3>
<p>服务器可能会返回一个请求的所有信息，有时是没有必要的。或者你需要运行多个请求来获取有用的信息。</p>
<h2 id=""><strong>总结</strong></h2>
<p>毫不意外API会在未来简化web通信。API的目的就是助力web应用的通信和数据的共享。</p>
<p>通过开发强大且富有创造性的系统，API帮助在线业务的发展。随着API架构的进化，会出现更加轻量、更灵活的变体，这对于手机应用和分散网络的发展至关重要。</p>
<p>在这篇文章中你学习了REST API的基础。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何使用 Node、Express 和 MongoDB 构建一个 RESTful API ]]>
                </title>
                <description>
                    <![CDATA[ 原文：How to Build a RESTful API Using Node, Express, and MongoDB [https://www.freecodecamp.org/news/build-a-restful-api-using-node-express-and-mongodb/] ，作者：Nishant Kumar [https://www.freecodecamp.org/news/author/nishant-kumar/] 在这篇文章中，我们将使用 Node、Express 和 MongoDB 构建一个 RESTful API。我们将为创建数据、读取数据、更新数据和删除数据（基本 CRUD 操作）创建端点（endpoints）。 但在我们开始之前，请确保你的系统中已经安装了 Node。如果没有，请到 https://nodejs.org/en/download/  下载并安装它。 让我们先做一下基本设置 在一个空文件夹中，运行以下命令： npm init 这个命令会问你各种细节，比如你的项目名称、作者、存储库等等。然后它将在该文件夹中生成一个 pa ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/build-a-restful-api-using-node-express-and-mongodb/</link>
                <guid isPermaLink="false">622f06b9e8c932065fba0076</guid>
                
                    <category>
                        <![CDATA[ RESTful API ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Express.js ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Mon, 14 Mar 2022 09:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/03/How-to-Build-a-Weather-Application-using-React--65-.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/build-a-restful-api-using-node-express-and-mongodb/">How to Build a RESTful API Using Node, Express, and MongoDB</a>，作者：<a href="https://www.freecodecamp.org/news/author/nishant-kumar/">Nishant Kumar</a></p><!--kg-card-begin: markdown--><p>在这篇文章中，我们将使用 Node、Express 和 MongoDB 构建一个 RESTful API。我们将为创建数据、读取数据、更新数据和删除数据（基本 CRUD 操作）创建端点（endpoints）。</p>
<p>但在我们开始之前，请确保你的系统中已经安装了 Node。如果没有，请到 <a href="https://nodejs.org/en/download/">https://nodejs.org/en/download/</a> 下载并安装它。</p>
<h2 id="">让我们先做一下基本设置</h2>
<p>在一个空文件夹中，运行以下命令：</p>
<pre><code class="language-shell">npm init
</code></pre>
<p>这个命令会问你各种细节，比如你的项目名称、作者、存储库等等。然后它将在该文件夹中生成一个 <strong>package.json</strong> 文件。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screenshot-2022-02-19-130254.jpeg" alt="Screenshot-2022-02-19-130254" width="600" height="400" loading="lazy"></p>
<pre><code class="language-json">{
  "name": "rest-api-express-mongo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" &amp;&amp; exit 1"
  },
  "author": "",
  "license": "ISC"
}
</code></pre>
<p>Package.json file</p>
<p>这个 Package.json 文件将包含所有的脚本，如如何运行应用程序，或如何测试应用程序，以及所有的依赖。</p>
<p>我们现在需要安装一些依赖项。</p>
<pre><code class="language-shell">npm i express mongoose nodemon dotenv
</code></pre>
<p>这里，</p>
<ol>
<li>Express 将被用于中间件，以创建各种 CRUD 端点</li>
<li>Mongoose 用于使用各种查询来管理 MongoDB 中的数据</li>
<li>Nodemon 用于在我们每次保存文件时重新启动我们的服务器</li>
<li>Dotenv 管理 <strong>.env</strong> 文件</li>
</ol>
<p>因此，请继续安装它们。</p>
<p>在它们完成安装后，创建一个名为 <strong>index.js.</strong> 的文件，这将是我们应用程序的入口。</p>
<p>而在这个文件中，让我们添加 Express 和 Mongoose，并运行该文件。</p>
<pre><code class="language-js">const express = require('express');
const mongoose = require('mongoose');
</code></pre>
<p>现在，将 Express 的内容转移到一个名为 <strong>app</strong> 的新常量中。</p>
<pre><code class="language-js">const express = require('express');
const mongoose = require('mongoose');

const app = express();
</code></pre>
<p>现在，让我们修改这个文件，在 3000 端口监听。</p>
<pre><code class="language-js">const express = require('express');
const mongoose = require('mongoose');

const app = express();

app.use(express.json());

app.listen(3000, () =&gt; {
    console.log(`Server Started at ${3000}`)
})
</code></pre>
<p>现在，服务器被设置在 <strong>端口3000</strong>。让我们写脚本来启动我们的服务器。我们还添加了 <strong>app.use</strong>。在这里面，我们有一个代码片段，允许我们接受 JSON 格式的数据。</p>
<p>在package.json文件中，添加一个脚本，内容如下：</p>
<pre><code class="language-js">"scripts": {
    "start": "nodemon index.js"
},
</code></pre>
<p>这意味着我们可以<strong>使用 npm start 启动我们的服务器</strong>，它将使用我们之前安装的 Nodemon 包运行。</p>
<p>在终端中输入 npm start，我们将在终端中看到以下输出:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screenshot-2022-02-19-132326.jpeg" alt="Screenshot-2022-02-19-132326" width="600" height="400" loading="lazy"></p>
<h2 id="mongodb">如何配置 MongoDB 数据库</h2>
<p>现在，让我们来配置 mongoDB 数据库。前往 <a href="https://account.mongodb.com/account/login">https://account.mongodb.com/account/login</a> 并创建你的账户，如果你已经有一个账户，则可以登录。</p>
<p>登录后，我们要创建一个数据库。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screenshot-2022-02-19-132848.jpeg" alt="Screenshot-2022-02-19-132848" width="600" height="400" loading="lazy"></p>
<p>因此，创建一个 <strong>Free Shared Cluster</strong>。</p>
<p>它将会要求你输入用户名和密码，输入它们。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screenshot-2022-02-19-132958.jpeg" alt="Screenshot-2022-02-19-132958" width="600" height="400" loading="lazy"></p>
<p>然后，添加你的 IP 地址。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screenshot-2022-02-19-133131.jpeg" alt="Screenshot-2022-02-19-133131" width="600" height="400" loading="lazy"></p>
<p>点击完成并关闭。</p>
<p>我们的集群将需要一些时间来完成，所以让我们等待吧。同时，在项目文件夹中创建一个名为 <strong>.env</strong> 的文件。</p>
<p>并在集群主页中，点击连接按钮。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screenshot-2022-02-19-133319.jpeg" alt="Screenshot-2022-02-19-133319" width="600" height="400" loading="lazy"></p>
<p>将出现以下窗口：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screenshot-2022-02-19-133404.jpeg" alt="Screenshot-2022-02-19-133404" width="600" height="400" loading="lazy"></p>
<p>点击 MongoDB Compass，它将返回以下字符串。同时，下载并安装 MongoDB Compass。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screenshot-2022-02-19-133516.jpeg" alt="Screenshot-2022-02-19-133516" width="600" height="400" loading="lazy"></p>
<p>将你的用户名和密码添加到这个你以前使用过的字符串中。最后的连接字符串将看起来像这样:</p>
<pre><code class="language-js">mongodb+srv://nishant:********@cluster0.xduyh.mongodb.net/testDatabase
</code></pre>
<p>这里，nishant 是用户名，其次是密码，最后是数据库名称。</p>
<p>所以，把这个字符串粘贴到 <strong>.env</strong> 文件中。</p>
<pre><code class="language-js">DATABASE_URL = mongodb+srv://nishant:*******@cluster0.xduyh.mongodb.net/testDatabase
</code></pre>
<p>现在在 MongoDB Compass 中，也添加这个字符串。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screenshot-2022-02-19-134347.jpeg" alt="Screenshot-2022-02-19-134347" width="600" height="400" loading="lazy"></p>
<p>然后，点击 <code>Connect</code>。</p>
<p>在这里，我们将得到两个数据库，这是默认的。第三个将在以后自动创建。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screenshot-2022-02-19-134435.jpeg" alt="Screenshot-2022-02-19-134435" width="600" height="400" loading="lazy"></p>
<p>现在，让我们在脚本文件 index.js 中导入我们的 <strong>.env</strong> 文件的内容。</p>
<pre><code class="language-js">require('dotenv').config();

const mongoString = process.env.DATABASE_URL
</code></pre>
<p>在这里，我们将字符串存储到一个名为 <strong>mongoString.</strong> 的变量中。</p>
<p>现在，让我们使用 Mongoose 将数据库连接到我们的服务器。</p>
<pre><code class="language-js">mongoose.connect(mongoString);
const database = mongoose.connection
</code></pre>
<p>现在，我们必须根据我们的数据库连接是成功还是失败，抛出一个成功或错误信息。</p>
<pre><code class="language-js">database.on('error', (error) =&gt; {
    console.log(error)
})

database.once('connected', () =&gt; {
    console.log('Database Connected');
})
</code></pre>
<p>这里，<strong>database.on</strong> 意味着它将连接到数据库，如果连接失败，将抛出任何错误。而 <strong>database.once</strong> 意味着它将只运行一次。如果它成功了，它将显示一条信息：数据库已连接。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screenshot-2022-02-19-135414-1.jpeg" alt="Screenshot-2022-02-19-135414-1" width="600" height="400" loading="lazy"></p>
<p>以下是到此为止的全部代码：</p>
<pre><code class="language-js">require('dotenv').config();

const express = require('express');
const mongoose = require('mongoose');
const mongoString = process.env.DATABASE_URL;

mongoose.connect(mongoString);
const database = mongoose.connection;

database.on('error', (error) =&gt; {
    console.log(error)
})

database.once('connected', () =&gt; {
    console.log('Database Connected');
})
const app = express();

app.use(express.json());

app.listen(3000, () =&gt; {
    console.log(`Server Started at ${3000}`)
})
</code></pre>
<h2 id="endpoints">如何为端点（Endpoints）创建我们的路由</h2>
<p>创建一个名为 routes 的文件夹，并在里面制作一个名为 routes.js 的文件。</p>
<p>将此文件导入我们的主脚本文件 index.js 中。</p>
<pre><code class="language-js">const routes = require('./routes/routes');
</code></pre>
<p>另外，让我们使用这个路由文件（routes.js）。</p>
<pre><code class="language-js">const routes = require('./routes/routes');

app.use('/api', routes)
</code></pre>
<p>这里，这个 app.use 需要两样东西。一个是基础端点（base endpoint），另一个是路由的内容。现在，我们所有的端点将从'/api'开始。</p>
<p>我们会得到一个错误，因为我们在路由文件里面没有任何东西。所以，让我们添加它们。</p>
<pre><code class="language-js">const express = require('express');

const router = express.Router()

module.exports = router;
</code></pre>
<p>在这里，我们使用 Express 中的 Router，并且我们也使用 module.exports 导出了它。现在，我们的应用程序可以正常工作了。</p>
<h2 id="endpoints">如何编写我们的端点（Endpoints）</h2>
<p>现在，让我们在这个路由文件中写入我们的端点。我们将有五条路由用于以下 actions：</p>
<ol>
<li>将数据发布到数据库</li>
<li>从数据库中获取所有数据</li>
<li>获取基于 ID 的数据</li>
<li>基于 ID 更新数据</li>
<li>根据 ID 删除数据</li>
</ol>
<p>因此，让我们为这些 actions 创建路由：</p>
<pre><code class="language-js">//Post Method
router.post('/post', (req, res) =&gt; {
    res.send('Post API')
})

//Get all Method
router.get('/getAll', (req, res) =&gt; {
    res.send('Get All API')
})

//Get by ID Method
router.get('/getOne/:id', (req, res) =&gt; {
    res.send('Get by ID API')
})

//Update by ID Method
router.patch('/update/:id', (req, res) =&gt; {
    res.send('Update by ID API')
})

//Delete by ID Method
router.delete('/delete/:id', (req, res) =&gt; {
    res.send('Delete by ID API')
})
</code></pre>
<p>我们有五个方法，使用 REST 方法的 Post、Get、Patch 和 Delete。</p>
<p>这个路由器把路由作为第一个参数。然后，在第二个参数中，它正在接受一个回调。</p>
<p>在回调中，我们有一个 res 和一个 req。res 表示响应，req 表示请求。我们使用 res 来向我们的客户端，如 Postman，或任何前端客户端发送响应。而我们使用 req 来接收来自客户端应用程序（如 Postman）或任何前端客户端的请求。</p>
<p>然后在回调 body 中，我们要打印一条消息，说明是响应 API 消息。</p>
<p>保存这个，然后打开 Postman 来检查端点（endpoints）。如果你没有，请下载 <a href="https://www.postman.com/downloads/">Postman</a>。它是测试 API 端点的一个好工具。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screenshot-2022-02-19-141237.jpeg" alt="Screenshot-2022-02-19-141237" width="600" height="400" loading="lazy"></p>
<p>在地址栏中添加这个地址，然后点击发送，或按回车键。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screenshot-2022-02-19-141328.jpeg" alt="Screenshot-2022-02-19-141328" width="600" height="400" loading="lazy"></p>
<p>我们将在 Postman 的正文中得到这个消息，因为我们只是使用 res.send. 发送一个消息。</p>
<p>现在，让我们从一个客户应用中获取一个响应。让我们简单地打印一个 ID。</p>
<p>我们必须首先改变 <strong>getOne</strong> 函数。我们使用 <strong>req.params.id</strong> 获取 ID，然后使用 <strong>res.send.</strong> 将其发送到客户端应用程序。</p>
<pre><code class="language-js">//Get by ID Method
router.get('/getOne/:id', (req, res) =&gt; {
    res.send(req.params.id)
})
</code></pre>
<pre><code class="language-js">localhost:3000/api/getOne/1000
</code></pre>
<p>在地址栏中添加这个端点（endpoint）。这里，我们使用 <strong>getOne</strong> 端点，后面是 ID。然后，点击发送。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screenshot-2022-02-19-142619.jpeg" alt="Screenshot-2022-02-19-142619" width="600" height="400" loading="lazy"></p>
<p>我们将在 Postman 的响应 body 中获得 ID。</p>
<h2 id="">如何创建模型</h2>
<p>现在，让我们创建一个模型，它将定义我们的数据库结构。</p>
<p>创建一个名为 model 的文件夹，里面有一个名为 model.js 的文件。</p>
<pre><code class="language-js">const mongoose = require('mongoose');

const dataSchema = new mongoose.Schema({
    name: {
        required: true,
        type: String
    },
    age: {
        required: true,
        type: Number
    }
})

module.exports = mongoose.model('Data', dataSchema)
</code></pre>
<p>在这里，我们有一个定义数据库结构的模式，它有一个 <strong>name</strong> 和一个 <strong>age</strong> 属性。这两个字段都有类型，而且都是必填的。</p>
<p>然后，我们简单地导出模式模型。</p>
<p>现在，在 <strong>routes.js</strong> 文件中导入这个模型。</p>
<pre><code class="language-js">const Model = require('../models/model');
</code></pre>
<h2 id="post">如何向数据库 post 数据</h2>
<p>让我们使用刚刚创建的模型创建要发布（post）的数据体。</p>
<pre><code class="language-js">router.post('/post', (req, res) =&gt; {
    const data = new Model({
        name: req.body.name,
        age: req.body.age
    })
})
</code></pre>
<p>我们的 name 和 agg 是接受来自 <strong>req body</strong> 的 name 和 age。我们从客户端应用如<strong>Postman</strong>，或任何前端客户端如 <strong>React</strong> 或 <strong>Angular.</strong> 获得这些数据。</p>
<p>我们还将创建一个<strong>try-catch</strong>块来处理成功信息和错误。</p>
<pre><code class="language-js">//Post Method
router.post('/post', (req, res) =&gt; {
    const data = new Model({
        name: req.body.name,
        age: req.body.age
    })

    try{

    }
    catch(error){
        
    }
})
</code></pre>
<p>在尝试块中，我们使用 <strong>data.save()</strong> 来保存 <strong>data</strong>。然后，我们将数据存储在一个叫做 <strong>dataToSave</strong> 的常量中。</p>
<p>我们还将成功的消息与数据一起发送到响应体中（response body）。</p>
<p>在 catch 块中，我们将接收任何错误，如果我们得到任何错误。</p>
<pre><code class="language-js">//Post Method
router.post('/post', (req, res) =&gt; {
    const data = new Model({
        name: req.body.name,
        age: req.body.age
    })

    try {
        const dataToSave = data.save();
        res.status(200).json(dataToSave)
    }
    catch (error) {
        res.status(400).json({message: error.message})
    }
})
</code></pre>
<p>现在，让我们从 Postman 添加一些数据。但在这之前，这个函数需要异步工作。所以，我们将使用 async-await。</p>
<pre><code class="language-js">router.post('/post', async (req, res) =&gt; {
    const data = new Model({
        name: req.body.name,
        age: req.body.age
    })

    try {
        const dataToSave = await data.save();
        res.status(200).json(dataToSave)
    }
    catch (error) {
        res.status(400).json({message: error.message})
    }
})
</code></pre>
<p>如果我们在正文中添加数据并点击发送，我们将得到以下结果：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screenshot-2022-02-19-145714.jpeg" alt="Screenshot-2022-02-19-145714" width="600" height="400" loading="lazy"></p>
<p>它也在生成一个唯一的 ID。打开 MongoDB Compass 应用程序，你会看到数据库和你刚刚创建的这条记录。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screenshot-2022-02-19-150007.jpeg" alt="Screenshot-2022-02-19-150007" width="600" height="400" loading="lazy"></p>
<h2 id="">如何获得所有数据</h2>
<p>获取数据也很简单，只需几行代码：</p>
<pre><code class="language-js">router.get('/getAll', async (req, res) =&gt; {
    try{
        const data = await Model.find();
        res.json(data)
    }
    catch(error){
        res.status(500).json({message: error.message})
    }
})
</code></pre>
<p>这里，我们使用 <strong>Model.find</strong> 方法从数据库中获取所有数据。然后，我们将其以 JSON 格式返回。如果我们有一个错误，我们也会得到它。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screenshot-2022-02-19-150423.jpeg" alt="Screenshot-2022-02-19-150423" width="600" height="400" loading="lazy"></p>
<p>如果我们在 Postman 中调用这个端点（endpoint），我们将在 Postman 主体中得到一个对象的数组。</p>
<h2 id="id">如何根据 ID 获取数据</h2>
<p>这个方法也很简单。我们只需要在一个叫做 <strong>findById</strong> 的方法中传递文档的 ID，也就是 <strong>req.params.id</strong>。</p>
<pre><code class="language-js">//Get by ID Method
router.get('/getOne/:id', async (req, res) =&gt; {
    try{
        const data = await Model.findById(req.params.id);
        res.json(data)
    }
    catch(error){
        res.status(500).json({message: error.message})
    }
})
</code></pre>
<p>如果我们点击发送，我们将根据 ID 获得数据。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screenshot-2022-02-19-150808.jpeg" alt="Screenshot-2022-02-19-150808" width="600" height="400" loading="lazy"></p>
<h2 id="id">如何根据 ID 来更新和删除数据</h2>
<p>首先，让我们使用 <strong>补丁（patch）</strong> 方法来针对更新方法。</p>
<pre><code class="language-js">//Update by ID Method
router.patch('/update/:id', async (req, res) =&gt; {
    try {
        const id = req.params.id;
        const updatedData = req.body;
        const options = { new: true };

        const result = await Model.findByIdAndUpdate(
            id, updatedData, options
        )

        res.send(result)
    }
    catch (error) {
        res.status(400).json({ message: error.message })
    }
})
</code></pre>
<p>在这里，我们有三个参数传递给 <strong>findByIdAndUpdate</strong> 方法，我们用它来通过 ID 找到一个文档并更新它。</p>
<p>其中 <strong>req.params.id</strong> 是常量 id，<strong>updatedData</strong> 包含 req.body，还有 <strong>options</strong>，它指定了是否在 body 中返回更新的数据。</p>
<p>现在我们来测试一下。只要粘贴一个特定文件的 ID，然后点击发送，也要改变端点（endpoints）。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screenshot-2022-02-19-152717.jpeg" alt="Screenshot-2022-02-19-152717" width="600" height="400" loading="lazy"></p>
<p>我们正在使用一个 ID 进行更新，而且它正在被更新。</p>
<p>删除也很简单，让我们来实现它：</p>
<pre><code class="language-js">//Delete by ID Method
router.delete('/delete/:id', async (req, res) =&gt; {
    try {
        const id = req.params.id;
        const data = await Model.findByIdAndDelete(id)
        res.send(`Document with ${data.name} has been deleted..`)
    }
    catch (error) {
        res.status(400).json({ message: error.message })
    }
})
</code></pre>
<p>我们在这里获取 ID，然后使用 Model.findByIdAndDelete 来删除该字段，同时传递 ID。</p>
<p>我们将更新的数据存储在一个常量 <strong>data</strong> 中。</p>
<p>在响应中，我们将得到这样的消息：具有特定名称的文档已经被删除。</p>
<p>如果我们测试一下，我们会得到以下结果：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screenshot-2022-02-19-153557.jpeg" alt="Screenshot-2022-02-19-153557" width="600" height="400" loading="lazy"></p>
<p>所以，所有五个方法都完成了。我们可以发布数据和获取所有的数据（也基于 ID）。我们还可以更新它们和删除它们。</p>
<h3 id="">谢谢你阅读本文</h3>
<p>在这篇文章中，你了解了如何使用 Node、Express 和 MongoDB 设计和开发一个 RESTful API。</p>
<p>现在你可以使用这些端点来构建一个全栈应用程序，使用 Vanilla JavaScript、React、Angular、Next 或 Vue.js。</p>
<p>你也可以看看我关于同一主题的视频，<a href="https://youtu.be/paxagc55loU">RESTful APIs - 使用 Node、Express 和 MongoDB 构建 RESTful API</a></p>
<p>欢迎从 <a href="https://github.com/nishant-666/Rest-Api-Express-MongoDB">GitHub</a> 下载代码并进行实验。</p>
<blockquote>
<p>祝你学习愉快！</p>
</blockquote>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 做轮子·实现路由的两种思路 ]]>
                </title>
                <description>
                    <![CDATA[ > 本文指的路由，不是路由器，也不是PS4（5？），是特指后端API框架里的router，前端路由也有可以有借鉴的部分，但本文不承诺正确性。 今天不妨讨论个不那么大的话题，restful路由的实现。 我们眼中的路由是什么样子的？ 我们常见的路由是这个样子的 router.get('/what/i/want', ... ); router.get('/what/i/hate', ... ); router.get('/what/u/want', ... ); 我们今天就来讨论一下，当我们想访问 /what/i/want时，都有些什么玄机。 路由是怎么工作的 首先明确路由的目的，就是根据接收到的路径字符串，找到对应的业务处理逻辑。 为了弄明白业界常用框架对这一块，我专门查阅了若干开发框架的源码（express, koa, Gin, kratos, Lumen），最终概括成两种实现手段。  * Key - Value 型路由  * Tree 型路由 现在来分析一下两种路由的特性和适用场景。 Key - Value 型路由 这个模式，形如我们熟悉的hash tab ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-backend-router-work/</link>
                <guid isPermaLink="false">5f5103dccd07b005bfb5aeb6</guid>
                
                    <category>
                        <![CDATA[ 路由 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ RESTful API ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 正则表达式 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 开发者小蓝 ]]>
                </dc:creator>
                <pubDate>Sat, 13 Feb 2021 09:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/09/318-02.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <blockquote>
<p>本文指的路由，不是路由器，也不是PS4（5？），是特指后端API框架里的router，前端路由也有可以有借鉴的部分，但本文不承诺正确性。</p>
</blockquote>
<p>今天不妨讨论个不那么大的话题，<strong>restful路由</strong>的实现。</p>
<h4 id="">我们眼中的路由是什么样子的？</h4>
<p>我们常见的路由是这个样子的</p>
<pre><code>router.get('/what/i/want', ... );

router.get('/what/i/hate', ... );

router.get('/what/u/want', ... );
</code></pre>
<p>我们今天就来讨论一下，当我们想访问 <code>/what/i/want</code>时，都有些什么玄机。</p>
<h4 id="">路由是怎么工作的</h4>
<p>首先明确路由的目的，就是根据接收到的路径字符串，找到对应的业务处理逻辑。</p>
<p>为了弄明白业界常用框架对这一块，我专门查阅了若干开发框架的源码（express, koa, Gin, kratos, Lumen），最终概括成两种实现手段。</p>
<ul>
<li>Key - Value 型路由</li>
<li>Tree 型路由</li>
</ul>
<p><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/328/318-03.jpg" alt="318-03" width="1066" height="427" loading="lazy"></p>
<p>现在来分析一下两种路由的特性和适用场景。</p>
<h4 id="keyvalue">Key - Value 型路由</h4>
<p>这个模式，形如我们熟悉的<code>hash table</code>、字典，通过键值对维护这<code>uri</code>与函数的对应关系。</p>
<p><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/328/318-04.jpg" alt="318-04" width="882" height="373" loading="lazy"></p>
<p>我们理所当然地认为，只要确定的访问地址， 如<code>/what/i/want</code>, 就能立即找到对应的业务方法<code>functionA</code>，是个铁骨铮铮的 <code>O(1)</code>操作。</p>
<blockquote>
<p>当一个操作的复杂度与规模无关，就是一个O(1)的操作。比如无论我的 uri 地址有多少个，只要确定 uri 的值，总是能立即确定与之对应的方法。</p>
</blockquote>
<p><strong>然而遗憾的是，我就没见到有这么实现路由的。</strong></p>
<p>真实的情况是，</p>
<p><strong>Key - Value 型路由会从前到后一个一个比较，直到找到一个能匹配当前 uri 的key，再确定它对应的 function</strong></p>
<p>所以实际上它是一个 <code>O(n)</code> 操作，平均情况下需要比较 <code>N/2</code> 次才能定位。</p>
<blockquote>
<p>一个 <code>O(n)</code> 操作，其复杂度会随着数据规模的增大而线性增长，而我们普遍认为，在经过多次测量观察后，平均情况下会在 N/2 的位置上找到答案。</p>
</blockquote>
<p>疑惑归疑惑，正如本文的作者一样，做轮子人并不是疯了。</p>
<p><strong>之所以采用这种逐个匹配的模式，是因为我们的uri通过是不能确定的。</strong></p>
<p>比如， 一些<code>uri</code>里甚至包含了变量</p>
<pre><code>GET /staff/9527

GET /staff/709394
</code></pre>
<p>所以，我们通常是吧 <strong>Key - Value</strong>里面的<strong>Key</strong>，设计成<strong>正则表达式</strong>。</p>
<p>相应的，在理论研究中的<strong>Hash Table</strong>也会被<strong>顺序表</strong>替代。</p>
<p>总结起来就是，</p>
<hr>
<p>当我们访问一个确定的<code>uri</code>时，路由会尝试按顺序逐个匹配注册好的<strong>正则表达式</strong>，直到<code>match</code>到一个结果。</p>
<p>否则，返回 404</p>
<hr>
<p><strong>升华一下</strong></p>
<blockquote>
<p>如何压榨这种路由的性能？ 既然是按顺序逐个匹配，那我们就根据自己的业务特点，把可能访问量最大的接口注册到前面去。</p>
<p>也有一些框架是会动态调整顺序的，这就是题外话。</p>
</blockquote>
<p>--</p>
<h4 id="tree">Tree 型路由</h4>
<p>这个模式相对来说思路要骚一些。</p>
<p>它把<code>uri</code>按照<code>/</code>分割一个，在之前构建好的路由树里，一个节点一个几点的检索，直到成功走到一个叶子节点。</p>
<p><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/328/318-05.jpg" alt="318-05" width="744" height="644" loading="lazy"></p>
<p>与上面提到的<code>N/2</code>次比较相比，这个模式的比较次数比较稳定，就是<code>uri</code>的层数。</p>
<blockquote>
<p>对于 <code>/what/i/want</code>，我们认为它是一个 3 层的地址，如果这个地址是存在的，那么在比较3次以后，就能找到对应的答案。</p>
</blockquote>
<p><strong>真是 “遇事不决，数据结构” 啊</strong></p>
<p>这种树形的路由特别适合<code>restful</code>风格的API，因为我们设计<code>restful</code>接口的时候，通常每一层都有它自己的<strong>类聚含义</strong>，所以它会构建出一棵非常标致的树。</p>
<p><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/328/318-06.jpg" alt="318-06" width="766" height="252" loading="lazy"></p>
<p><strong>问(gang)题(jing)来了</strong></p>
<p>Q1: 请问 Tree 型路由 怎么解决<code>uri</code>里有变量的情况呢？</p>
<blockquote>
<p>很巧妙。当你注册的路由包含了变量，比如 <code>/staff/:id</code>这样的，它会在这个树<code>staff</code>的子节点里，也就第二层，创建一个通配节点（可能是<code>*</code>）。</p>
<p>它认为，不管<code>staff</code>后面跟什么内容，都能匹配这个<code>*</code></p>
</blockquote>
<p>Q2: 如果我同时注册了<code>/staff/:id</code>和<code>/staff/list</code>呢？会匹配错吗？</p>
<blockquote>
<p>考虑到这种情况，Tree 型路由的一般实现是，优先匹配确定值<code>list</code>，次要匹配<code>*</code>。</p>
<p>有这种情况是要复杂一些，不过并不影响它的整体复杂度。</p>
</blockquote>
<h4 id="">最后，作者喜欢哪一种？</h4>
<p>尽管我在最近开发的一个框架里使用了<strong>Tree 型路由</strong>，这并不代表我的倾向。</p>
<p>实际上应该根据上文提到两种模式各自的特点，选择最好的实现方式。</p>
<p><strong>当然</strong></p>
<p>我期待读者自己做轮子的时候，能做出一个根据开发者注册的路由的特点，自动选择路由模型的<strong>智能路由</strong>。</p>
<hr>
<p>好了，今天先到这里，下次再会。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
