Fastify and mongodb
In this article I will show you how to use Fastify and MongoDB together to build a CRUD plugin!
This post has been inspired by
fastify-in-practice
A talk I gave at the Come to Code 2021 conference in Italy!
Let's jump into the code!
TODO List application
We will create a simple TODO list application using Fastify and MongoDB. It will be a simple CRUD application within 4 routes:
- GET /todos : returns all the todos
- POST /todos : creates a new todo
- PUT /todos/:id : updates the todo with the given id
- DELETE /todos/:id : deletes the todo with the given id
Our basic object model will be:
const model = {
id: 'unique-identifier',
text: 'todo item text',
done: false,
doneAt: null
}
Setup
First of all, we need to create a fastify application. So let's create a brand new project and install fastify:
mkdir my-fastify-app
cd my-fastify-app
npm init --yes
npm install fastify
Then we will need some additional plugins to boost our productivity!
npm install fastify-cli
npm install fastify-env
npm install fastify-mongodb
Note that we will use a local mongodb installation, so we need to install mongodb locally or run a Docker container image.
Build the application scaffold
We are not repeating the nice Getting Started documentation from the Fastify website. In fact, we are going to use since the beginning the fastify-cli
to build our application!
Let's create an app.js
file:
module.exports = async function application (app, opts) {
app.get('/', async (request, reply) => {
return { hello: 'world' }
})
// our code..
}
Now, let's add to the package.json
these scripts:
{
"scripts": {
"start": "fastify start -l info --options app.js",
"dev": "fastify start -l info --options app.js --watch --pretty-logs",
}
}
We will be able to run npm run dev
to start the application in development mode. This will enable us to watch for changes and restart the application automatically!
Connect to MongoDB
Connecting Fastify to a MongoDB database is very easy! Let's edit out app.js
and add the following code:
module.exports = async function application (app, opts) {
// ...
app.register(require('fastify-mongodb'), {
url: 'mongodb://localhost:27017/todo-list'
})
// our code..
}
Now to start the server we need a mongodb instance running. So, we can add two new commands to the scripts
section of the package.json
:
{
"scripts": {
"mongo:start": "docker run --rm -d -p 27017:27017 --name mongo-todo mongo:4",
"mongo:stop": "docker stop mongo-todo"
}
}
Running npm run mongo:start
will start a mongodb container and npm run mongo:stop
will stop it.
Adding the configuration
Every application needs a configuration file. We have set the mongodb URL into out code, but this is not a good practice. We should use environment variables instead.
Create a new .env
file and add the following lines:
NODE_ENV="development"
MONGO_URL="mongodb://localhost:27017/todo-list"
Now let's update the app.js
file:
module.exports = async function application (app, opts) {
// ...
await app.register(require('fastify-env'), {
schema: {
type: 'object',
properties: {
PORT: { type: 'integer', default: 3000 },
NODE_ENV: { type: 'string' },
MONGO_URL: { type: 'string' }
}
}
})
app.register(require('fastify-mongodb'), {
url: app.config.MONGO_URL
})
// our code..
}
Using the fastify-env
plugin we can now access the environment variables from the app.config
object. Moreover, it will trigger an error if the environment variables are not set correctly.
How to create the routes
We have a working application, but we need to add some routes. Create a new file routes.js
and add the following code:
module.exports = function todoRoutes (app, opts, next) {
app.post('/todos', async function createTodo (request, reply) {
const todosCollection = app.mongo.db.collection('todos')
const result = await todosCollection.insertOne(request.body)
reply.code(201)
return { id: result.insertedId }
})
app.get('/todos', async function readTodos (request, reply) {
const todosCollection = app.mongo.db.collection('todos')
const docs = await todosCollection.find().toArray()
return docs.map(d =>
// remove the _id field and name it as id
d.id = d._id.toString()
return d
})
})
app.put('/todos/:id', async function updateTodo (request, reply) {
const todosCollection = app.mongo.db.collection('todos')
const result = await todosCollection.updateOne(
{ _id: this.mongo.ObjectId(request.params.id) },
{
$set: {
done: request.body.done,
doneAt: request.body.done === true ? new Date() : null
}
})
// returns 404 is the todo is not found
if (result.matchedCount === 0) {
const error = new Error('Object not found: ' + request.params.id)
error.status = 404
throw error
}
return { id: request.params.id }
})
app.delete('/todos/:id', async function deleteTodo (request, reply) {
const todosCollection = this.mongo.db.collection('todos')
const result = await todosCollection.deleteOne({
_id: this.mongo.ObjectId(request.params.id)
})
if (result.deletedCount === 0) {
const error = new Error('Object not found: ' + request.params.id)
error.status = 404
throw error
}
return { id: request.params.id }
})
next()
}
The file routes.js
is a Fastify plugin. Everything in Fastify is a plugin, and even our routes can be a plugin.
The fastify-mongodb
plugin adds a decorator to our app
instance called mongo
. The mongo
object will contain a db
property, which is a mongodb database instance - which we have set through the .env
file.
Now we can test our routes calling them from an HTTP client as curl
:
curl \
-X POST http://localhost:3000/todos \
-H 'Content-Type: application/json' \
-d '{"text":"LEARN FASTIFY"}'
curl http://localhost:3000/todos
curl \
-X PUT http://localhost:3000/todos/$(id) \
-H 'Content-Type: application/json' \
-d '{"done":true}'
curl -X DELETE http://localhost:3000/todos/$(id)
Secure the API
At the moment, we have a very simple API. But it lacks security. To secure our API we need to add the JSON Schema Validation and Serialization to our routes!
Validation
The validation protect our routes from bad input data. We can define a set out schemas, using the JSON Schema standard.
In a schema.js
file we can add the following code:
const todoInputSchema = {
type: 'object',
additionalProperties: false,
properties: {
text: {
type: 'string',
minLength: 1,
maxLength: 80
},
done: {
type: 'boolean',
default: false
}
},
required: ['text']
}
const todoUpdateSchema = {
type: 'object',
additionalProperties: false,
properties: {
done: {
type: 'boolean',
default: false
}
}
}
const todoIdSchema = {
type: 'object',
properties: {
id: {
type: 'string',
minLength: 24,
maxLength: 24
}
}
}
const todosArraySchema = {
type: 'array',
items: {
type: 'object',
additionalProperties: false,
properties: {
id: { type: 'string' },
text: { type: 'string' },
done: { type: 'boolean' },
doneAt: { type: 'date-time' }
}
}
}
These schemas will:
- remove all unknown properties from the inputs, so we can be sure that we are only working with the defined properties
- validate the input data against the schema, so we can be sure that the data is correct and we are not inserting a TODO item with a text that is too long
- for the GET route, it returns to the client only the properties that are defined in the schema
To integrate the schemas within our routes, we need to edit the routes.js
file:
// ...
app.post('/todos', {
schema: {
body: schemas.todoInputSchema
}
}, ...)
app.get('/todos', {
schema: {
response: {
200: schemas.todosArraySchema
}
}
}, ...)
app.put('/todos/:id', {
schema: {
params: schemas.todoIdSchema,
body: schemas.todoUpdateSchema
}
}, ...)
app.delete('/todos/:id', {
schema: {
params: schemas.todoIdSchema
}
}, ...)
And it is done!! If you try to call your routes with bad data, you will get a 400
error.
Summary
Congratulations! You have completed the Fastify tutorial! Now you can use Fastify to build your API and secure it.
You have seen very useful features of Fastify:
- application reloading during development
- how to connect and use the MongoDB database
- how to use the JSON Schema Validation and Serialization
See you soon for the next article about testing and deployment! The code is available on the fastify-in-practice
repository.
Top comments (0)