开始使用Fastify Node.js框架和Faunadb
在庞大的生态系统中选择合适的JavaScript技术栈是具有挑战性的。由于Fastify的易用性和良好的开发者体验,最近流行起来的Node.js网络服务器框架之一是Fastify。
在本教程中,我们将使用Fauna创建一个包含用户文档的User 集合,保护路由,读取和删除集合中的用户文档。
先决条件
- 对JavaScript编程语言的基本理解是必不可少的。
- 在你的系统中安装任何最新版本的Node.js运行时。
- 安装一个文本编辑器,如VS Code。
- 你需要一个API客户端,如Postman。在我的案例中,我将使用Thunder Client,它可以作为VSCode扩展。
- FaunaDB是一个云数据库。因此,你至少需要一个基本的免费级别的账户。
项目设置
为了开始,创建你的项目文件夹并命名为fastify-fauna ,在你喜欢的IDE中打开它,并确保从终端访问它。
用命令npm init -y 来初始化NPM。这应该在你的项目文件夹中创建一个初始的package.json 文件。
接下来,我们将需要安装我们的应用程序npm 的依赖性,包括。
fastify:Fastify是一个Node.js网络框架,具有良好的开发者体验和强大的插件架构。faunadb:FaunaDB客户端是一个Node.js数据库驱动,与Fauna云多模型数据库互动。dotenv:API密钥在任何应用程序中都是敏感数据,因此不应该硬编码或推送到任何Github仓库。dotenv是一个模块,它将环境变量从.env文件注入到我们的Node.js应用程序中。
要使用npm安装这些包,请运行该命令。
npm install fastify faunadb dotenv
接下来,在根目录下创建一个条目文件index.js ,并添加以下代码来启动我们的服务器。
const app = require('fastify')({ logger: true })
const startServer = async ()=> {
try {
await app.listen(3000)
app.log.info(`Listening on ${fastify.server.address().port}`)
} catch (err) {
app.log.error(err)
process.exit(1)
}
}
startServer()
简单地剖析一下上面的代码。
-
我们导入fastify模块,将
app对象创建为const app = require('fastify')({ logger: true })。logger被作为一个可选的参数传递,以确保我们在控制台中获得我们应用程序的日志。 -
在监听
port 3000上的请求之前,fastify期望有一个异步函数(async function startServer()),在我们的服务器无法启动的情况下,这个异步函数将解决或拒绝。
稍后,我们将使用命令node index.js 来启动我们的服务器并检查是否一切正常。
创建Fauna初始数据库
你需要创建一个免费账户来访问fauna仪表板,以便创建一个新的数据库。
最初,仪表板的屏幕应该是这样的。

Fauna 仪表板
接下来,给你的数据库起个名字。将数据库命名为FASTIFY-FAUNA 。

在Fauna中创建新的数据库
由于我们需要从我们的Node.js应用代码中访问我们的Fauna数据库,我们可以创建一个服务器访问密钥。
在你的Fauna仪表板上,导航到Security 部分并创建一个新的密钥。确保使用其Role 选项作为Server 。

创建一个安全密钥
该密钥是一个秘密,是我们用来从Node.js访问Fauna的东西。
注意:服务器密钥应该被安全地保存,因为Fauna不会再显示它。
从这里,我们现在已经准备好执行我们的数据库查询。使用我们仪表板中的shell,我们需要创建我们的fauna数据库集合和索引。
一个集合存储用户的文件。为了创建Users 集合,打开shell并运行查询。
CreateCollection({
name: "Users"
})
当我们执行上述查询时,它应该返回。

在数据库中创建新的集合
接下来,我们需要一个数据库索引。这是从一个表中复制的数据列,主要是为了实现非常有效的搜索和唯一的条目。
CreateIndex({
name: "Users_by_username",
source: Collection("Users"),
terms: [{field: ["data", "username"]}],
unique: true
})
我们的服务器密钥需要存储在一个.env 文件中。
创建它并添加你的服务器秘密。
FAUNA_SECRET=fnAEL1cTZWACBe86wLa_EgUk6JAz8IebvKlQAK-u
你应该使用你从仪表板上获得的秘密,因为我将会删除这个。
在我们的.env 文件中的任何环境变量将在我们的代码中使用process.env 。为了防止包含我们敏感的服务器信息的.env 文件被推送到Github仓库,创建一个.gitignore ,并添加如下所示。
.env
node_modules
创建我们的自定义错误类
在我们可以开始处理我们的服务器路由端点之前,我们需要处理可能从Fauna收到的任何预期错误。
为了实现这一点,我们将使用一个自定义的FaunaError 类,它集成到Fastify的错误处理生命周期中。
添加文件夹和一个文件作为errors/FaunaError.js ,并粘贴以下代码。
class FaunaError extends Error {
constructor (err) {
super()
const errors = err.requestResult.responseContent.errors
this.code = errors[0].code
this.message = errors[0].description
this.statusCode = 500
if (this.code === 'instance not unique'){
this.statusCode = 409
}
if (this.code === 'authentication failed') {
this.statusCode = 401
}
if (this.code === 'unauthorized') {
this.statusCode = 401
}
if (this.code === 'instance not found') {
this.statusCode = 404
}
if (this.code === 'permission denied') {
this.statusCode = 403
}
}
}
module.exports = FaunaError
这个类检查从Fauna错误实例返回的HTTP状态和描述。Fastify将读取statusCode 属性,并在请求失败时将其作为响应的HTTP代码返回。
在Fauna中创建用户
现在让我们创建我们的第一个Fastify REST路由,它发出一个POST 请求来创建新用户。
app.post('/users', require('./routes/create-user.js'))
现在,创建一个routes 文件夹,在里面添加routes/create-user.js 文件。
粘贴代码。
const faunadb = require('faunadb')
const FaunaError = require('../errors/FaunaError.js')
// destructure Create and Collection
const {Create, Collection} = faunadb.query
module.exports = {
schema: {
body: {
type: 'object',
required: ['username', 'password'],
properties: {
username: {type: 'string'},
password: {
type: 'string',
minLength: 8
}
}
}
},
async handler (request, reply) {
const {request} = body
const {username, password} = request
const client = new faunadb.Client({
secret: process.env.FAUNA_SECRET
})
try {
const result = await client.query(
Create(
Collection('Users'),
{
data: {username},
credentials: {password}
}
)
);
reply.send(result)
} catch (error) {
throw new FaunaError(error)
}
}
}
这是一个公共路由,因此,我们的服务器秘密将被用来执行查询。一旦我们的用户通过了认证,我们将使用他们生成的秘密来运行查询。
授权规则将只允许用户执行在某个端点上被允许的操作。稍后会有更多这方面的内容。
Fauna使用一个与其他数据库客户端不同的HTTP引擎,我们将需要在每次请求数据库时实例化一个新的客户端。它运行在云端,因此每个数据库查询都只是一个HTTP请求。
如果从数据库返回一个错误,我们需要捕捉它,在把它记录到控制台之前实例化FaunaError类。Fastify应该会处理剩下的事情,因为日志记录已经启用。
为了在开发过程中测试我们的路由,一个HTTP客户端是有用的。我的首选工具是Thunder客户端,它可以作为VS Code的扩展。
让我们向http://localhost:3000/users 发出一个POST请求,其正文是一个JSON对象。不要忘记添加Content-Type 头。
{
"username": "Winchy",
"password": "supersecretpassword"
}
如果一切按照我们的预期进行,我们的响应体应该包含一个在我们的Users 集合中创建的JSON文档表示。
{
"ref": {
"@ref": {
"id": "301556533148254727",
"collection": {
"@ref": {
"id": "Users",
"collection": {
"@ref": {
"id": "collections"
}
}
}
}
}
},
"ts": 1606435813770000,
"data": {
"username": "Winchy"
}
}

创建新用户的请求-响应
试图两次创建同一个用户应该返回一个Fauna错误,因为Users_by_username 索引不允许两个文档存在相同的用户名。
添加用户认证
在做进一步的请求之前,让我们用Fastify创建一个端点,来验证我们的用户。
将这段代码添加到index.js 文件中。
app.post('/login', require('./routes/login.js'))
这是路由文件夹中的一个/login POST HTTP端点,将用于验证用户的身份。对于这个路由,在我们的routes 目录内创建一个login.js 文件并添加代码。
const faunadb = require('faunadb')
const FaunaError = require('../errors/FaunaError.js')
const {Login, Match, Index} = faunadb.query
module.exports = {
schema: {
body: {
type: 'object',
required: ['username', 'password'],
properties: {
username: {type: 'string'},
password: {type: 'string'}
}
}
},
async handler (request, reply) {
const {request} = body
const {username, password} = request
const client = new faunadb.Client({
secret: process.env.FAUNA_SECRET
});
try {
const result = await client.query(
Login(
Match(Index('Users_by_username'), username), // match by username and password
{password}
)
)
reply.send({
secret: result.secret
})
} catch (error) {
throw new FaunaError(error)
}
}
}
在这里,我们使用索引Users_by_username 与声明性的Login() 函数,该函数是由faunadb.query 对象实例解构的。然后,FAUNA_SECRET ,在认证前需要传递给客户端实例。
一旦来自请求体的用户名和密码匹配,用户认证就应该成功完成。这将返回一个服务器秘密,以便将来为该用户进行请求。
让我们尝试一下,向http://localhost:3000/login 发出一个POST请求。
我们请求的JSON主体应该是。
{
"username": "Winchy",
"password": "supersecretpassword"
}
如果这是成功的,API应该返回一个包含用户秘密的响应体。
{
"secret": "fnED7o254PACAAPuFGfOAAIDnuZTNlU5Z7dD3LdjPKycDCyUkeI"
}
客户端需要存储生成的秘密,并将其用于对API的进一步请求。在接下来的路线中会有更多关于这个的内容。
这是一个简单的基本认证形式。在你的生产应用中,你应该决定哪种认证策略对你的用例更有效。始终使用HTTPS与服务器和第三方API进行交互。
检索用户文件
与我们之前的路由不同,我们将使这个端点成为一个私有路由。在Fastify中使用私有路由的推荐方法是使用钩子。它们作为自定义的代码位,将在请求/响应生命周期的某些点上执行。
钩子是使用.addHook 方法注册的,它可以帮助我们在Fastify的生命周期内进行交互和监听事件。
我们的钩子将首先检查fauna-secret 头是否存在于我们标记为私有的所有路由上。.decorateRequest() 方法会创建一个装饰器,让Fastify知道我们的请求对象将是一个被修改的对象。
实现这一目的的代码应该在index.js 文件中。
app.addHook('onRequest', async (request, reply) => {
if (!reply.context.config.isPrivateRoute) return
const faunaSecret = request.headers['fauna-secret']
if (!faunaSecret) {
reply.status(401).send()
return
}
request.faunaSecret = faunaSecret
})
app.decorateRequest('faunaSecret', '')
如果我们碰巧使用了一个无效的秘密,Fauna应该返回一个错误响应。
在进行GET 请求之前,将其添加到index.js 文件中。
app.get('/users/:userId', require('./routes/get-user.js'))
同时创建带有该代码的routes/get-user.js 文件。
const faunadb = require('faunadb')
const FaunaError = require('../errors/FaunaError.js')
const {Get, Ref, Collection} = faunadb.query
module.exports = {
config: {
isPrivateRoute: true
},
schema: {
params: {
type: 'object',
required: ['userId'],
properties: {
userId: {
type: 'string', // use type and pattern
pattern: "[0-9]+"
}
}
}
},
async handler (request, reply) {
const userId = request.params.userId
const client = new faunadb.Client({
secret: request.faunaSecret
})
try {
const result = await client.query(
Get(
Ref(
Collection('Users'),
userId
)
)
)
reply.send(result)
} catch (error) {
throw new FaunaError(error)
}
}
}
一个简短的代码演练。
- 我们把我们的Fauna客户端和
FaunaError类导入到我们的get-user.js文件。 - 我们从
fauna.query对象中导入了Get,Ref, 和Collection方法,用于向Fauna数据库进行查询。 - 在我们路由的配置部分,我们添加了属性
isPrivateRoute,表示该路由对于我们的自定义钩子是私有的。 - 另外,提供的用户秘密被用来与Fauna通信。
如果我们在这个路由上尝试这个请求,Fauna会回应一个错误,因为我们的用户目前没有读取集合的权限Users 。一个快速的解决方法是在Fauna中像一个新的自定义角色一样修改这个。
在Faunadb中设置授权
要从仪表板上添加授权,请到仪表板的Roles 标签下的Security 部分,并点击New Custom Role 来启用它。
指定一个名称为User ,添加集合为Users ,并点击阅读权限来启用它。

创建新的自定义角色
Fauna将需要知道谁属于所添加的角色。在仪表板的Membership 标签下添加它,并确保Users 选项集合被选为这个角色的成员。

创建新的成员集合
任何用基于Users 集合中的文档的令牌登录的用户都将拥有该集合中任何文档的读取权限。我的前一个用户的文档ID刚好是301556533148254727 。
对于你的情况,通过回到Fauna仪表板的集合部分检查用户的ID ,并从该文档中抓取。
在我们做这个HTTP请求之前,我们需要确保我们添加了用户的秘密(我们在登录用户后得到的秘密),并将其添加到自定义fauna-secret HTTP头中。
现在,我们需要向端点GET HTTP请求,http://localhost:3000/users/301556533148254727 。
该请求应该在URI中响应文件ID 。
{
"ref": {
"@ref": {
"id": "301556533148254727",
"collection": {
"@ref": {
"id": "Users",
"collection": {
"@ref": {
"id": "collections"
}
}
}
}
}
},
"ts": 1606435813770000,
"data": {
"username": "Winchy"
}
}

获取用户详细信息的GET请求
删除一个用户
要进行删除请求,首先,我们需要在自定义角色中添加允许我们删除User 的权限。
在仪表板上,回到Security 部分,修改Users 集合上的Roles ,以允许删除请求。保存修改内容,并将DELETE HTTP路由添加到我们的index.js 文件。
app.delete('/users/:userId', require('./routes/delete-user.js'));
最后在路由文件夹内创建delete-user.js 文件并粘贴代码。
const faunadb = require('faunadb')
const FaunaError = require('../errors/FaunaError.js')
const {Delete, Ref, Collection} = faunadb.query
module.exports = {
config: {
isPrivateRoute: true
},
async handler (request, reply) {
const userId = request.params.userId
const client = new faunadb.Client({
secret: request.faunaSecret
})
try {
const deleteResult = await client.query(
Delete(
Ref(
Collection('Users'),
userId
)
)
)
reply.send(deleteResult)
} catch (error) {
throw new FaunaError(error)
}
}
}
我们的端点使用来自faunadb.query 对象的Delete 函数来匹配作为参数传递的userId 。向http://localhost:3000/users/301556533148254727 端点发出DELETE 请求,并响应删除的文件。
如果我们试图使用一个已删除用户的秘密,我们将得到一个401 的错误,表明该请求是未经授权的。
结论
FaunaDB云数据库的简单性提供了很好的体验,同时又不牺牲可扩展性和简单性。它是一个完整的无服务器的、快速的、符合ACID标准的数据库,可以在云端无限扩展,而不需要开发者过多的麻烦。
另一方面,Fastify在Node.js网络服务器中提供了高性能和令人发指的速度。它丰富的插件生态系统确保我们不会重新发明车轮。
担心对开发者友好吗?
嗯,就像Express一样,Fastify是简单而优雅的,有很好的日志系统。我们还能要求什么呢?
我希望你在你的下一个Node.js项目中尝试一下。