A easy step-by-step tutorial
Photo by Kelly Sikkema on Unsplash

Presumably, no web developer is a stranger to REST APIs and the challenges that architecting an effective and efficient API solution bring.

These challenges include:

  • Speed. (API response times.)
  • Documentation. (Clear concise documents, describing the API.)
  • Architecture and sustainability. (Maintainable and expandable codebase.)

In this tutorial, we are going to address all of the above using a combination of Node.jsMongoDBFastify, and Swagger.


Before We Begin

You should have beginner to intermediate JavaScript knowledge, have heard of Node.js and MongoDB, and know what REST APIs are.

The technology we will use

It is a good idea to open the above pages in new tabs, for easy reference.

You will need to have the following installed

You will also need an IDE and a terminal,I use iTerm2 for Mac and Hyper for Windows.


Let’s Get Started

Photo by Beau Runsten on Unsplash

Initialize a new project by opening your terminal and executing the following lines of code:

mkdir fastify-api
cd fastify-api
mkdir src
cd src
touch index.js
npm init

In the above code, we created two new directories, navigated into them, created an index.js file, and initialed our project via npm.

You will be prompted to enter several values when initializing a new project, these you can leave blank and update at a later stage.

Once completed, a package.json file is generated in the src directory. In this file you can change the values entered when the project is initialized.

Next, we install all the dependencies that we will need:

npm i nodemon mongoose fastify fastify-swagger boom

Below you can see start script from package.json file which will start nodemon server and load index.js file with it:

“start”: “./node_modules/nodemon/bin/nodemon.js ./src/index.js”,

Our package.json file should now look as follows:

{
  "name": "fastify-api",
  "version": "1.0.0",
  "description": "A blazing fast REST APIs with Node.js, MongoDB, Fastify and Swagger.",
  "main": "index.js",
  "scripts": {
  "start": "./node_modules/nodemon/bin/nodemon.js ./src/index.js",
  "test": "echo \"Error: no test specified\" && exit 1"
},
  "author": "Borrowed Code  (www.borrowedcode.com)",
  "license": "ISC",
  "dependencies": {
  "boom": "^7.2.2",
  "fastify": "^1.13.0",
  "fastify-swagger": "^0.15.3",
  "mongoose": "^5.3.14",
  "nodemon": "^1.18.7"
  }
}

Set Up up the Server and Create the First Route

Photo by Matt Duncan on Unsplash

Add the following code to your index.js file.

// Require the framework and instantiate it
const fastify = require('fastify')({
  logger: true
})

// Declare a route
fastify.get('/', async (request, reply) => {
  return { hello: 'world' }
})

// Run the server!
const start = async () => {
  try {
    await fastify.listen(3000)
    fastify.log.info(`server listening on ${fastify.server.address().port}`)
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}
start()

We require the Fastify framework, declare our first route, and initialize the server on port 3000, the code is quite self-explanatory but take note of the options object passed when initializing Fastify:

// Require the fastify framework and instantiate it
const fastify = require('fastify')({
  logger: true
})

The above code enables Fastify’s built-in logger, which is disabled by default.

You can now run the following code in your src directory in your terminal:

npm start

Now, when you navigate to http://localhost:3000/ you should see the {hello:world} object returned.

We will get back to the index.js file but, for now, let’s move on to setting up our database.


Start MongoDB and Create the Model

Photo by Jan Antonin Kolar on Unsplash

Once MongoDB has been successfully installed, you can open a new terminal window and start up a MongoDB instance by running the following:

mongod

With MongoDB, we do not need to create a database. We can just specify a name in the setup and, as soon as we store data, MongoDB will create this database for us.

Add the following to your index.js file:

...
// Require external modules
const mongoose = require('mongoose')
// Connect to DB
mongoose.connect(‘mongodb://localhost/mycargarage’)
 .then(() => console.log(‘MongoDB connected…’))
 .catch(err => console.log(err))
...

In the above code, we require Mongoose and connect to our MongoDB database. The database is called mycargarage and, if all went well, you will now see MongoDB connected…

Notice that you did not have to restart the app, thanks to the Nodemon package we added earlier.

Now that our database is up and running, we can create our first model. Create a new folder within the src directory, and called models, and within it create a new file called Car.js and add the following code:

// External Dependancies
const mongoose = require('mongoose')

const carSchema = new mongoose.Schema({
  title: String,
  brand: String,
  price: String,
  age: Number,
  services: {
    type: Map,
    of: String
  }
})

module.exports = mongoose.model('Car', carSchema)

The above code declares our carSchema that contains all the information related to our cars. Apart from the two obvious data types: String and Number.

We also make use of a Map which is relatively new to Mongoose and you can read more about it here. We then export our carSchema to be used within our app.

We could proceed with setting up our routes, controllers, and config in the index.js file, but part of this tutorial is demonstrating a sustainable codebase. Therefore, each component will have its own folder.


Create the Controller

Photo by Thái An on Unsplash

To get started with creating the controllers, we create a folder in the src directory called controllers, and within the folder, we create a carController.js file, and added this code:

// External Dependancies
const boom = require('boom')

// Get Data Models
const Car = require('../models/Car')

// Get all cars
exports.getCars = async (req, reply) => {
  try {
    const cars = await Car.find()
    return cars
  } catch (err) {
    throw boom.boomify(err)
  }
}

// Get single car by ID
exports.getSingleCar = async (req, reply) => {
  try {
    const id = req.params.id
    const car = await Car.findById(id)
    return car
  } catch (err) {
    throw boom.boomify(err)
  }
}

// Add a new car
exports.addCar = async (req, reply) => {
  try {
    const car = new Car(req.body)
    return car.save()
  } catch (err) {
    throw boom.boomify(err)
  }
}

// Update an existing car
exports.updateCar = async (req, reply) => {
  try {
    const id = req.params.id
    const car = req.body
    const { ...updateData } = car
    const update = await Car.findByIdAndUpdate(id, updateData, { new: true })
    return update
  } catch (err) {
    throw boom.boomify(err)
  }
}

// Delete a car
exports.deleteCar = async (req, reply) => {
  try {
    const id = req.params.id
    const car = await Car.findByIdAndRemove(id)
    return car
  } catch (err) {
    throw boom.boomify(err)
  }
}

The above may seem like a little much to take in, but it is actually really simple.

  • We require boom to handle our errors: boom.boomify(err)
  • We export each of our functions that we will use in our route.
  • Each function is an async function which can contain an await expression that pauses the execution of the async function and waits for the passed promise’s resolution, and then resumes the async function’s execution and returns the resolved value. Learn more here.
  • Each function is wrapped in a try & catch statement. Learn more here.
  • Each function takes two parameters: req (the request) and reply (the reply). In our tutorial, we only make use of the request parameter. We will use it to access the request body and the request parameters, allowing us to process the data. Learn more here.
  • Take note of the code on this line: const car = new Car({ …req.body }) This makes use of the JavaScript spread operator. Learn more here.
  • Take note of the code on this line:const { …updateData } = carThis makes use of the JavaScript destructuring, in conjunction with the spread operator. Learn more here.

Other than that, we make use of some standard Mongoose features to manipulate our database.

You are probably burning to fire up your API and do a sanity check, but before we do this, we just need to connect the controller to the routes and then, lastly, connect the routes to the app.


Create and import the routes

Photo by Egor Myznik on Unsplash

Once again, we can start by creating a folder in the root directory of our project, but this time, it is called routes. Within the folder, we create an index.js file with the following code:

// Import our Controllers
const carController = require('../controllers/carController')

const routes = [
  {
    method: 'GET',
    url: '/api/cars',
    handler: carController.getCars
  },
  {
    method: 'GET',
    url: '/api/cars/:id',
    handler: carController.getSingleCar
  },
  {
    method: 'POST',
    url: '/api/cars',
    handler: carController.addCar,
    schema: documentation.addCarSchema
  },
  {
    method: 'PUT',
    url: '/api/cars/:id',
    handler: carController.updateCar
  },
  {
    method: 'DELETE',
    url: '/api/cars/:id',
    handler: carController.deleteCar
  }
]

module.exports = routes

Here, we require our controller and assign each of the functions that we created in our controller to our routes.

As you can see, each route consists of a method, a URL, and a handler, instructing the app which function to use when one of the routes is accessed.

The :id following some of the routes is a common way of passing parameters to the routes, and this will allow us to access the idas follows:

http://127.0.0.1:3000/api/cars/5bfe30b46fe410e1cfff2323

Testing the API

Photo by National Cancer Institute on Unsplash

Now that we have most of our parts constructed, we just need to connect them all together to start serving data via our API.

Firstly, we need to import the routes we created by adding the following line of code to our main index.js file:

const routes = require(‘./routes’)

We then need to loop over our routes array to initialize them with Fastify.We can do this with the following code, which also needs to be added to the main index.js file:

routes.forEach((route, index) => {
 fastify.route(route)
})

Now we are ready to start testing!

The best tool for the job is Postman, which we will use to test all of our routes. We will be sending our data as raw objects in the body of the request and as parameters.

Remember api url is http://127.0.0.1:3000

Finding all cars:

Finding a single car:

Adding a new car:

The services appear to be empty, but the information does, in fact, persist to the database.

Adding a new car:

Deleting a car:


Adding Swagger and finishing up

Photo by iMattSmart on Unsplash

We now have a fully functional API — but what about the documentation? This is where Swagger is really handy.

Now, we will create our final folder called config. Inside, we will create a file called swagger.js with the following code:

exports.options = {
  routePrefix: '/documentation',
  exposeRoute: true,
  swagger: {
    info: {
      title: 'Fastify API',
      description: 'Building a blazing fast REST API with Node.js, MongoDB, Fastify and Swagger',
      version: '1.0.0'
    },
    externalDocs: {
      url: 'https://swagger.io',
      description: 'Find more info here'
    },
    host: 'localhost',
    schemes: ['http'],
    consumes: ['application/json'],
    produces: ['application/json']
  }
}

The above code is an object with the options which we will pass into our fastify-swaggerplugin. To do this, we need to add the following to our index.js file:

// Import Swagger Options
const swagger = require(‘./config/swagger’)
// Register Swagger
fastify.register(require(‘fastify-swagger’), swagger.options)

And then, we need to add the following line after we have initialized our Fastify server:

...
await fastify.listen(3000)
fastify.swagger()
fastify.log.info(`listening on ${fastify.server.address().port}`)
...

As simple as that! You now have self-updating API documentation that will evolve with your API. You can easily add additional information to your routes, see more here.


Conclusion

Now that we have a basic API in place, the possibilities are limitless. It can be used as the base for any app imaginable.