使用 Fastify 构建 CRUD API
大家好,本文我们将使用Fastify构建一个 NodeJS CRUD API。Fastify是一个用于构建快速 NodeJS 服务器的框架。借助这个强大的工具,您可以创建 NodeJS 服务器、创建路由(端点)、处理对每个端点的请求等等。
Fastify 是 Express(L express)的替代方案,如果您之前接触过 NodeJS,那么您应该听说过 Express。事实上,Fastify 的设计灵感正是来源于 Express,只是 Fastify 服务器的速度比 Express 服务器快得多。
我测试过,可以证明它的速度很快。我目前正在开发一个移动应用,在这个应用中,我使用 Fastify 作为我的 API。
因此,在本文中,我们将使用 Fastify 构建一个基本的 NodeJS 服务器。该服务器将包含创建数据、读取数据、更新数据和删除数据(CRUD)的接口。我们还将使用 JWT 进行一些身份验证(将在下一篇文章中介绍),以此向您展示 Fastify 插件生态系统及其强大之处。
前提条件:
在使用 Fastify 之前,您需要了解哪些内容?
- JavaScript:你应该掌握相当数量的 JavaScript 知识,尤其是 ES5 和 ES6。Codecademy上有很多很棒的课程可以指导你。
- NodeJS:您还应该熟悉NodeJS。您可以在Codecademy上找到NodeJS课程。
- Express:这完全是可选的,但如果你已经了解 Express,那么你学习 Fastify 粘贴的速度就会更快。
介绍就到此为止,让我们直接进入代码部分。
请在 GitHub 上查看完整代码
熟悉 Fastify
设置应用程序
就像我们用 Express 创建服务器并使用简单的端点来测试它是否运行一样,我将向您展示如何使用 Fastify 实现同样的功能。我们将初始化服务器,注册端口并监听该端口上的事件。
npm init -y让我们初始化一个 package.json 文件。你可以在终端中执行此操作,这将创建一个名为 package.json 的文件,其中包含一些关于你的应用程序的 JSON 信息。
现在我们来使用 NPM 安装 Fastify。您也可以使用 yarn。使用以下npm install Fastify命令安装 Fastify。我们还要安装的其他软件包有:
- nodemon:用于在每次更改后自动重启服务器。我们将把此软件包作为开发依赖项安装。使用 NPM 是
npm install -D nodemon…… - 配置:用于存储密钥。当您想要发布到 GitHub 时非常有用。安装方法如下:
npm install config
其他软件包将在需要时引入和安装。接下来,我们来设置 package.json 文件。
打开 package.json 文件,将 `<server_name>` 的值更改为main` server.js<server_name>`,因为我们将在其中创建服务器的文件名为 server.js。此外,删除 ` test<server_name>` 属性及其值。将以下代码粘贴到 ` script<server_name>` 属性中。
"start": "node server.js",
"server": "nodemon server.js"
这意味着,当我们npm start在终端运行该命令时,它会运行即将创建的server.jsnpm run server文件。但是,当我们在终端运行该命令时,它会使用 nodemon 来运行server.js文件。
现在创建一个server.js文件,准备使用 Fastify 创建你的第一个 NodeJS 服务器。
创建我们的服务器
我们进入server.js文件并导入 Fastify。
const fastify = require('fastify')({ logger: true });
该logger: true;关键值是一个选项,用于启用 Fastify 在我们的终端上进行日志记录。这样,请求信息、服务器启动信息、响应信息和错误信息都将被记录到终端中。
接下来,我们需要将端口号分配给一个PORT变量,我这里用的是 5000。之所以要创建一个变量,是为了方便部署到生产环境。所以你应该会看到类似这样的代码const PORT = process.env.PORT || 5000。这样一来,我们要么使用的是托管公司(例如 Heroku 或 Digital Ocean)提供的端口,要么使用的是我们自定义的 5000 端口。
现在让我们创建一个简单的路由,用于向/.
fastify.get('/', (req, reply) => {
reply.send('Hello World!');
});
是不是很眼熟?看起来和 Express 很像吧?没错,所以对于已经熟悉 Express 的人来说,使用 Fastify 会非常容易,因为它们的语法很相似。
req`and`reply分别代表请求和回复(响应)。它们显然是参数,所以你可以随意命名。但我们建议使用这种简洁易读的形式。
好了,现在让我们通过监听事件来启动服务器。我们之前会fastify.listen(port)监听服务器收到的请求。但是这个函数会返回一个 Promise 对象,所以我们需要创建一个使用 async 和 await 来处理这个 Promise 的函数。
const startServer = async () => {
try {
await fastify.listen(PORT);
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
你需要确保记录错误日志,并在发生错误时退出服务器。现在我们可以直接在终端上调用startServer()并运行npm run server命令来启动服务器。
你应该能在终端的日志信息中看到你的 URL 地址,就像上图所示,或者直接使用 `.` 命令http://localhost:5000。使用你选择的任何 API 测试工具进行测试,你应该会收到“Hello world”作为响应。
创建更多路线
你肯定不希望所有的路由都放在server.js文件里,所以我们会创建一个名为routes 的文件夹。我们将用它来管理和组织 API 的所有不同路由。
这个 API 将用于博客,所以我们的数据主要关于博文以及发布这些博文的管理员。因此,在routes文件夹中,创建 posts.js 和admins.js文件。
为了让这些文件作为服务器端点正常工作,我们需要将它们注册为插件。别担心,这比你想象的要简单。只需将以下代码添加到server.js 文件中的相应函数之前即可startServer。
fastify.register(require('./routes/posts')); // we will be working with posts.js only for now
这样就能注册 POST 路由。你可以先导入并将其赋值给一个变量,然后将该变量作为参数传递给register函数,选择权在你。
如果保存,将会产生错误,这是因为我们还没有在posts.js中创建任何路由。
在posts.js 文件中,创建一个名为 `fastify` 的函数postRoutes,并传递三个参数:`fastify`、`options`和 ` done` 。这个函数会创建一个 fastify 服务器实例,这意味着通过第一个参数,我们可以实现之前在server.js文件中使用 `options` 变量所能做的一切fastify。
现在你可以将server.js中的 get 请求剪切到posts.jspostRoutes中的函数中。
你的postRoutes文件应该看起来像这样:
const postRoutes = (fastify, options, done) => {
fastify.get('/', (req, reply) => {
reply.send('Hello world');
});
};
options (有时写作opts )参数用于路由选项,我们不会使用它。
`done`参数是一个函数,我们会在函数结束时调用它postRoutes,以表明程序已完成。这就像在 Express 中创建一个中间件,然后调用 `next` 继续执行一样。
所以你应该把它done()放在函数的最后一行postRoutes。
现在,让我们导出该函数并保存文件。在posts.js文件的最后一行使用以下命令进行导出:module.exports = postRoutes.
保存文件并测试路线。
路线规划
我们可以创建更多类似上面那样的路由,然后就此收工,但这会让我们无法充分利用 Fastify 的一些强大功能。借助 Fastify,我们可以通过分离关注点来更好地组织 API。
借助 Fastify,我们可以为路由接收到的请求和路由发送的响应创建模式。对于请求,我们可以告诉 Fastify 请求体、请求头、请求参数等应该包含哪些内容。
我们还可以告诉 Fastify 我们打算发送什么作为响应,例如在 200 响应、400 响应或 500 响应等情况下将发送的数据。
例如,让我们为上面的 GET 请求创建一个模式。在我们的 GET 请求中,我们发送了“Hello world”(一个字符串)作为响应,现在我们将发送一个帖子数组,如下所示。
fastify.get('/', (req, reply) => {
reply.send([
{ id: 1, title: 'Post One', body: 'This is post one' },
{ id: 2, title: 'Post Two', body: 'This is post two' },
{ id: 3, title: 'Post Three', body: 'This is post three' },
]);
});
我们来创建它的模式。在 Fastify 中,模式是一个对象,这个对象将作为属性的值传递schema。
const opts = {
schema: {},
};
const postRoutes = (fastify, options, done) => {
fastify.get('/', opts);
done();
};
我们将以这种方式定义我们的路由,get 方法(可以是 post 或任何方法)将接受两个参数,第一个参数是路由,最后一个参数是选项对象。
我们将在此 API 中使用的选项对象的三个属性是:
-
schema:定义了我们的数据应该如何设置,哪些数据应该输入,哪些数据应该输出,包括它们的类型(字符串、布尔值、数字等)。 -
preHandler:一个定义在以下函数处理请求之前应该执行的操作的函数handler。 -
handler:处理请求的函数。
你现在可能还不清楚,但我们举个例子你就会明白了。这个preHandler将用于身份验证,这意味着它只会用于受保护的路由。
解释就到此为止,如果想了解更多,请查看文档。接下来我们直接看代码。
我们的 GET 请求即将变得更好。
const opts = {
schema: {
response: {
200: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'number' },
title: { type: 'string' },
body: { type: 'string' },
},
},
},
},
},
handler: (req, reply) => {
reply.send([
{ id: 1, title: 'Post One', body: 'This is post one' },
{ id: 2, title: 'Post Two', body: 'This is post two' },
{ id: 3, title: 'Post Three', body: 'This is post three' },
]);
},
};
虽然现在情况有所改善,但我估计也更令人困惑了。好吧,很简单,我们来分析一下模式对象。
模式
在模式对象中,我们告诉 Fastify,当响应状态码为 200 时,我们将发送一个数组。该数组中的每个元素都是一个对象,这些对象的属性分别为 `a` id、 `b`title和`c`,body它们的类型分别为 `a` number、string`b` 和`c` string。
很简单,对吧?你应该记下所用属性的名称,例如 `a` response、200`b` 和type`c`。`a` 和 ` itemsc`properties可以是任何名称,但我建议使用这些名称。
如果你尝试id从模式对象中移除该属性及其值,你会发现该id属性不再作为响应的一部分发送。而如果你尝试将该id属性的类型从 `type`更改number为 `type` string,你会在响应中看到它以字符串的形式出现。很酷吧!
处理程序
处理函数非常清晰,我们只是简单地复制了我们在 GET 请求中的代码。
opts对象是特定于某个路由的。除非你想用同一个响应处理不同路由上的不同请求。如果不是这种情况,那么你应该确保该对象的名称是唯一的。
例如,在我们的 GET 请求中,由于我们获取的是帖子,我们可以将名称更改为getPostsOpts。
我们的posts.js 文件现在应该看起来像这样
const getPostsOpts = {
schema: {
response: {
200: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'number' },
title: { type: 'string' },
body: { type: 'string' },
},
},
},
},
},
handler: (req, reply) => {
reply.send([
{ id: 1, title: 'Post One', body: 'This is post one' },
{ id: 2, title: 'Post Two', body: 'This is post two' },
{ id: 3, title: 'Post Three', body: 'This is post three' },
]);
},
};
const postRoutes = (fastify, options, done) => {
fastify.get('/', getPostsOpts);
done();
};
现在想象一下,你有 10 条路由,每条路由都有不同的模式和处理程序,可能还有一些预处理程序。你可以想象,代码会变得非常复杂,难以阅读。这时,控制器就派上用场了。
Controllers 并非像听起来那样是一种插件或包。它只是一个文件夹,我们用它来将路由与模式和处理程序分开。
在controllers文件夹内,我将创建两个名为 schemas 和 handlers 的文件夹。这样可以使目录结构更清晰,更易于阅读。
在我们的schemas文件夹中,我们将创建一个名为 posts.js 的文件。该文件将包含我们所有帖子路由的 schema(获取所有帖子、创建帖子、删除帖子等)。
在schemas/posts.js 文件中,创建一个名为 `<object>` 的对象getPostsSchema,并将 `<property>` 属性的值schema(来自routes/posts.js 文件)剪切下来,粘贴到该对象中。你的代码应该如下所示:
const getPostsSchema = {
response: {
200: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'number' },
title: { type: 'string' },
body: { type: 'string' },
},
},
},
},
};
现在我们把它导出;
const getPostsSchema = {
// our schemas
};
module.exports = { getPostsSchema };
我们将在routes/posts.js文件中导入它,以便我们可以将其用作属性的值schema。
const { getPostsSchema } = require('../controllers/schemas/posts.js');
const getPostsOpts = {
schema: getPostsSchema,
handler: (req, reply) => {
reply.send([
{ id: 1, title: 'Post One', body: 'This is post one' },
{ id: 2, title: 'Post Two', body: 'This is post two' },
{ id: 3, title: 'Post Three', body: 'This is post three' },
]);
},
};
在我们的handlers文件夹中,我们创建一个名为posts.js 的文件。该文件将包含我们所有 post 路由的处理函数(获取所有帖子、创建帖子、删除帖子等)。
在handlers/posts.js 文件中,创建一个名为 `get_posts_name` 的函数getPostsHandler,并将 `$_posts_name`req和reply`$_posts_name` 作为参数。从routes/posts.js文件中复制函数体并粘贴到这里,然后导出该函数。它应该看起来像这样:
const getPostsHandler = (req, reply) => {
reply.send([
{ id: 1, title: 'Post One', body: 'This is post one' },
{ id: 2, title: 'Post Two', body: 'This is post two' },
{ id: 3, title: 'Post Three', body: 'This is post three' },
]);
};
module.exports = { getPostsHandler };
将其导入getPostsHandler到routes/posts.js文件中,并将其设置为处理程序方法的值。您的routes/posts.js 文件应如下所示:
const { getPostsSchema } = require('../controllers/schemas/posts.js');
const { getPostsHandler } = require('../controllers/handlers/posts.js');
const getPostsOpts = {
schema: getPostsSchema,
handler: getPostsHandler,
};
const postRoutes = (fastify, opts, done) => {
fastify.get('/', getPostsOpts);
done();
};
这样看起来更简洁了吧?现在保存文件并测试一下,应该和以前一样可以正常工作了。
我很想在这里谈谈如何组织身份验证,但这会让这篇文章太长,所以我将另写一篇文章来讨论身份验证。
好了,Elijah,我们能直接构建CRUD API了吗?当然可以!
使用 Fastify 构建您的第一个 CRUD API
我们将创建一个博客 API,用于创建文章、读取所有文章、阅读文章、删除文章和更新文章。我们还将能够创建管理员、登录管理员以及创建受保护的路由。但这些内容将在另一篇文章中介绍。
查看所有帖子
由于我们已经有了可用的 GET 请求,我将对路由和帖子数组进行一些更改。
在routes/posts.js中。
fastify.get('/api/posts', getPostsOpts);
这样应该能让路由看起来更像一个 API 端点。
让我们在根目录下创建一个名为cloud 的文件夹,并在其中创建一个名为posts.js 的文件。这个文件将作为我们的数据库,因为我们会将所有文章存储在其中。将以下代码粘贴到该文件中:
const posts = [
{ id: 1, title: 'Post One', body: 'This is post one' },
{ id: 2, title: 'Post Two', body: 'This is post two' },
{ id: 3, title: 'Post Three', body: 'This is post three' }, // you can add as many as you want
];
module.exports = posts;
在handlers/posts.js 文件中,导入 posts 并将其替换为函数中的数组send,即
在handlers/posts.js中。
const posts = require('../../cloud/posts.js');
const getPostsHandler = (req, reply) => {
reply.send(posts);
};
module.exports = { getPostsHandler };
保存文件并运行程序,请注意路线已更改。要获取所有帖子,请使用http://localhost:your_port/api/posts
注意:有四个名为posts.js 的文件。
- cloud/posts.js:存储帖子数组的地方(我们的数据库)。
- routes/posts.js:我们在这里处理博客文章的所有路由。
- handlers/posts.js:我们在这里处理对 post 路由的响应。
- schemas/posts.js:我们在此处指定 post 路由的模式。
我会将他们每个人与各自的文件夹对应起来,方便您区分。
获取帖子
接下来我们要做的就是获取一篇帖子,我们会使用帖子的 ID。所以我们会id从请求中获取一个 ID 参数,然后筛选数组posts来找到这篇帖子。
在routes/posts.js中创建路由
在routes/posts.js 文件中,紧接第一个路由下方,粘贴以下代码。
fastify.get('/api/posts/:id', getPostOpts); // the :id route is a placeholder for an id (indicates a parameter)
让我们创建getPostOpts对象
const getPostOpts = {
schema: getPostSchema, // will be created in schemas/posts.js
handler: getPostHandler, // will be created in handlers/posts.js
};
在schemas/posts.js中创建模式
创建一个名为 `<object>` 的对象getPostSchema,并将以下内容粘贴到其中。
const getPostSchema = {
params: {
id: { type: 'number' },
},
response: {
200: {
type: 'object',
properties: {
id: { type: 'number' },
title: { type: 'string' },
body: { type: 'string' },
},
},
},
};
`params` 属性指示路由参数中应收集哪些数据。我用它来将 id 格式化为数字。默认值为字符串。由于我们文章数组中的 id 都是数字,所以我希望它们的数据类型相同。
由于我们只获取一个帖子,这意味着我们的响应将是一个包含 id、title 和 body 三个属性的对象。导出时getPostSchema,只需将其添加到要导出的对象中即可。module.exports = { getPostsSchema, getPostSchema };
现在仔细查看一下你的routes/posts.js 文件,你会发现你重复编写了代码。所以重构一下,确保没有重复,我就是这么做的。
const typeString = { type: 'string' }; // since i will be using this type a lot
const post = {
type: 'object',
properties: {
id: { type: 'number' },
title: typeString,
body: typeString,
},
};
const getPostsSchema = {
response: {
200: {
type: 'array',
items: post,
},
},
};
const getPostSchema = {
params: {
id: { type: 'number' },
},
response: {
200: post,
},
};
module.exports = { getPostsSchema, getPostSchema };
在handlers/posts.js中创建处理程序
在handlers/posts.js 文件中,创建一个名为 `posts` 的对象getPostHandler,并将以下内容粘贴到其中。
const getPostHandler = (req, reply) => {
const { id } = req.params;
const post = posts.filter((post) => {
return post.id === id;
})[0];
if (!post) {
return reply.status(404).send({
errorMsg: 'Post not found',
});
}
return reply.send(post);
};
函数体的第一行代码用于从请求路由中获取 id。因此,像这样的路由http://localhost:5000/api/posts/4将返回 id 为 4。
该reply.status函数告诉 Fastify 响应应该是什么状态码。如果找不到 POST 请求,则会发送自定义错误消息;使用 Fastify,我们还可以……
return reply.status(404).send(new Error('Post not found'));
因此,当找不到帖子时,Fastify 会发送以下 JSON 作为响应。
{
"statusCode": 404,
"error": "Not Found",
"message": "Post not found"
}
现在导出getPostHandler并保存所有文件。运行程序并测试你的新路线。
创建新帖子
在routes/posts.js中创建路由
首先,让我们在函数中创建路由postRoutes。在上一个创建的路由之后,粘贴以下代码。
fastify.post('/api/posts/new', addPostOpts);
/api/posts/new这是我们向帖子数组添加新帖子的端点。接下来,我们需要addPostOpts在路由函数之外创建对象,并传递模式和处理程序的值。
const addPostOpts = {
schema: addPostSchema, // will be created in schemas/posts.js
handler: addPostHandler, // will be created in handlers/posts.js
};
在我的下一篇文章中,我将把这条路由设为私有路由,这意味着我们将preHandler在下一篇文章中向上面的对象添加一个。
在schemas/posts.js中创建模式
我们将告诉 Fastify 请求正文中应该包含哪些数据,以及我们将作为响应发送哪些数据。
创建一个名为`<object>`的对象addPostSchema,并将以下代码赋值给它;
const addPostSchema = {
body: {
type: 'object',
required: ['title', 'body']
properties: {
title: typeString, // recall we created typeString earlier
body: typeString,
},
},
response: {
200: typeString, // sending a simple message as string
},
};
我们使用 ` bodyrequest_body` 作为属性,告诉 Fastify 应该从 POST 路由的请求体中获取什么内容。就像我们params上面所做的那样。我们也可以对 `request_body` 做同样的操作headers(我将在身份验证期间向您展示这一点)。
通过该required属性,我们告诉 Fastify,如果两者都不是请求正文的一部分title,则返回错误。body
如果缺少必填字段,Fastify 将返回400 Bad Request错误作为响应。
添加到从此文件( schemas/posts.jsaddPostSchema )导出的对象中。
在handlers/posts.js中创建处理程序
我们将为收到的数据创建一个 ID,并将其添加到我们的帖子数组中。很简单,对吧!
const addPostHandler = (req, reply) => {
const { title, body } = req.body; // no body parser required for this to work
const id = posts.length + 1; // posts is imported from cloud/posts.js
posts.push({ id, title, body });
reply.send('Post added');
};
添加到从该文件( handlers/posts.jsaddPostHandler )导出的对象中。
在保存文件并运行程序之前,请确保将 ` addPostSchemaand`添加addPostHandler到导入到routes/posts.js 的对象中。
要验证您的帖子是否已创建,您可以运行http://localhost:your_port/api/posts(我们的第一个端点),您会在数组底部看到它。
更新帖子
在routes/posts.js中创建路由
我们将使用这种put方法来处理这条路由。请将以下代码添加到您的postRoutes函数中。
fastify.put('/api/posts/edit/:id', updatePostOpts);
接下来,我们需要在函数updatePostOpts外部创建对象postRoutes。和之前一样,我们会为 ` schemaand`handler属性传递一个值,即 `ie`。
const updatePostOpts = {
schema: updatePostSchema, // will be created in schemas/posts.js
handler: updatePostHandler, // will be created in handlers/posts.js
};
在切换到其他文件之前,请快速updatePostSchema将以下内容添加updatePostHandler到此文件(routes/posts.js)中的导入对象。
在schemas/posts.js中创建模式
创建一个名为 `<object>` 的对象updatePostSchema,并使用以下代码来操作它。
const updatePostSchema = {
body: {
type: 'object',
required: ['title', 'body'],
properties: {
title: typeString,
body: typeString,
},
},
params: {
id: { type: 'number' }, // converts the id param to number
},
response: {
200: typeString, // a simple message will be sent
},
};
别忘了在updatePostSchema要导出的对象中添加该属性。
在handlers/posts.js中创建处理程序
const updatePostHandler = (req, reply) => {
const { title, body } = req.body;
const { id } = req.params;
const post = posts.filter((post) => {
return post.id === id;
})[0];
if (!post) {
return reply.status(404).send(new Error("Post doesn't exist"));
}
post.title = title;
post.body = body;
return reply.send('Post updated');
};
别忘了在updatePostHandler要导出的对象中添加该属性。
现在您可以保存文件并测试新路线了。
删除帖子
在routes/posts.js中创建路由
我们将沿用之前路线的相同流程,只会改变路线和方法。
fastify.delete('/api/posts/:id', deletePostOpts);
该deletePostOpts对象将是
const deletePostOpts = {
schema: deletePostSchema,
handler: deletePostHandler,
};
在schemas/posts.js中创建模式
需要注意的是,创建模式是完全可选的,对于这样的路由,您可能不需要创建模式。
const deletePostSchema = {
params: {
id: { type: 'number' }, // converts the id param to number
},
response: {
200: typeString,
},
};
在handlers/posts.js中创建处理程序
const deletePostHandler = (req, reply) => {
const { id } = req.params;
const postIndex = posts.findIndex((post) => {
return post.id === id;
});
if (postIndex === -1) {
return reply.status(404).send(new Error("Post doesn't exist"));
}
posts.splice(postIndex, 1);
return reply.send('Post deleted');
};
导出你的处理程序和模式,并相应地在routes/posts.js中导入它们。保存文件并测试你的新路由。
结语
这是我对本文的最后总结,并非针对 Fastify 的总结。我们尚未添加涉及身份验证的管理员路由。我们将在下一步添加此功能,因此请务必关注其发布通知。
综上所述,我要祝贺你使用 Fastify 构建了你的第一个 CRUD API。在这个项目中,我们创建了创建数据、读取数据、更新数据和删除数据的路由。我们也简单介绍了一下 Fastify。干得漂亮!
如果您觉得这篇文章有用,请点赞并分享。您也可以请我喝杯咖啡以示支持。感谢阅读,祝您编程愉快!
文章来源:https://dev.to/elijahtrillionz/build-a-crud-api-with-fastify-688
