Project Kenneth

Project Kenneth

Building an API using Express and MongoDB

Building an API using Express and MongoDB

In this post, we'll go through the process of creating an API built using Express and MongoDB.

We'll cover the ff. steps:

  1. Setting up MongoDB
  2. Creating an Express application
  3. Optimizing your Express routes
  4. Handling errors

Let's start!

Setting up MongoDB

In this section, we'll go through creating and configuring a remote MongoDB instance.

Using a MongoDB instance hosted on the cloud

Instead of installing a local MongoDB instance, we'll use Atlas which is MongoDB's official database-as-a-service.

  1. Create a MongoDB Atlas account here.
  2. Create a cluster. For new accounts, you can use the forever free tier!
  3. Create the super admin user.

Managing your databases using Compass

To better visualize our data, we'll be using the official GUI for MongoDB, Compass.

  1. Download the latest Compass version here.
  2. Install the thing!
  3. Get the database connection string from Atlas.
    1. Access your Atlas dashboard. Then, on your cluster panel, click the Connect button.
    2. On the Connect popup, create your super admin user.
    3. Then, for the Connection Method, choose Connect using MongoDB Compass.
    4. Then, choose the latest Compass version and then copy the connection string.
    5. Replace the credentials in the connection string with your actual credentials.
    6. Keep the connection string somewhere safe so you can use it in the next steps.
  4. Launch Compass, **key in your connection string, then, click Connect**. DB 1.2.4 Connect to Cluster.PNG
  5. Once connected, you can now click on the Create Database button.
  6. Specify the database name and the first collection's name. Then, click the Create Database button on the popup.
    1. For this example, I created a database named audit-log-demo and a collection named user-profile. DB 1.2.6 Create Database.PNG
  7. You should now see audit-log-demo as part of the database list.
  8. Now, let's add test data to our database.

    1. Click on the audit-log-demo database. You will be directed to the collection list page.
    2. Click on the user-profile collection. You will be directed to the collection management page.
    3. Under the Documents tab, click on the Add Data > Insert Document button.
    4. In the Insert to Collection popup, paste the following properties just below the _id property:

       "firstName": "Tony",
       "lastName": "Stark",
       "age": 25
      

      DB 1.2.8.4 Collection List.PNG

Creating an Express application

In this section, let's go through the step-by-step process of creating an Express application and letting this application establish a connection to our new MongoDB instance.

Setting up

  1. Open your favorite CLI and navigate to your desired working directory.
  2. Create a new package using npm init. Follow the prompts and provide the necessary details.
  3. Install both express and the mongodb driver by executing npm install mongodb express --save
  4. Get the database's connection string from Atlas.
    1. Access your Atlas dashboard. Then, on your cluster panel, click the Connect button.
    2. Then, for the Connection Method, choose Connect your application.
    3. Then, choose the appropriate NodeJS version and then copy the connection string.
    4. Replace the credentials in the connection string with your actual credentials.
    5. Keep the connection string somewhere safe so you can use it in the next steps.
  5. Create a new environment setting with key ALD_CONN_STRING and set its value to your connection string.

Connecting to the database

  1. At the root of your working directory, create an index.js file with this content:

     const { MongoClient, ObjectId } = require('mongodb');
     const express = require('express');
    
     const mongoConnString = process.env.ALD_CONN_STRING;
     const mongoClient = new MongoClient(mongoConnString);
    
     const expressApp = express();
     const expressPort = 3000;
    
     expressApp.get('/profile', async (req, res, next) => {
         try {
             await mongoClient.connect();
    
             const db = mongoClient.db('audit-log-demo');
             const col = db.collection('user-profile');
    
             const profileList = await col.find({}).toArray();
    
             res.send({
                 data: profileList
             });
         } catch (err) {
             next(err);
         }
         finally {
             await mongoClient.close();
         }
     });
    
     expressApp.listen(expressPort, () => {
         console.log(`Example app listening at http://localhost:${expressPort}`)
     });
    

    In the above code, we used the ALD_CONN_STRING environment variable to retrieve the connection string. Then, we instantiated the MongoDB and Express clients. We also introduced one route (/profiles) which retrieves all the documents in the user-profile collection.

  2. Run your application by executing node index.js on your CLI.
  3. Then, using your favorite REST client (I'm using Postman), access the /profiles endpoint of your API. You should get this result:

     {
         "data": [
             {
                 "_id": "<GUID>",
                 "firstName": "Tony",
                 "lastName": "Stark",
                 "age": 25
             }
         ]
     }
    

Adding a new route

To further expand the capabilities of the API, we add a new route to get a specific profile by ID.

To do this, we just need to add the following code to your index.js file just before the listen call:

expressApp.get('/profile/:id', async (req, res, next) => {
    try {
        await mongoClient.connect();

        const db = mongoClient.db('audit-log-demo');
        const col = db.collection('user-profile');

        const profile = await col.findOne({ _id: ObjectId(req.params.id) });

        res.send({
            data: profile
        });
    } catch (err) {
        next(err);
    }
    finally {
        await mongoClient.close();
    }
});

Checkpoint

You can check out the index.js code at this point by clicking here.

Optimizing your Express routes

At this stage, the 2 routes we created are as follows:

expressApp.get('/profiles', async (req, res, next) => {
    try {
        await mongoClient.connect();

        const db = mongoClient.db('audit-log-demo');
        const col = db.collection('user-profile');

        const profileList = await col.find({}).toArray();

        res.send({
            data: profileList
        });
    } catch (err) {
        next(err);
    }
    finally {
        await mongoClient.close();
    }
});

expressApp.get('/profile/:id', async (req, res, next) => {
    try {
        await mongoClient.connect();

        const db = mongoClient.db('audit-log-demo');
        const col = db.collection('user-profile');

        const profile = await col.findOne({ _id: ObjectId(req.params.id) });

        res.send({
            data: profile
        });
    } catch (err) {
        next(err);
    }
    finally {
        await mongoClient.close();
    }
});

The above code works, but, there's one major point of improvement in them.

In both routes, the parts that connect to the database and retrieve a reference to the collection are repeated. We should always follow the DRY (Don't Repeat Yourself) principle!

So how should we go about this? We introduce middleware to the code!

What's a middleware?

In Express, a middleware is a function that can be executed before or after the actual request handlers.

For our example, we need to define 2 middleware functions:

  1. A middleware that establishes the connection to the database and will then pass this connection instance to the request handlers.
  2. A middleware that closes the connection to the database. This middleware function will be executed after the request handlers.

Let's add in the middleware functions

Here's the code for the 2 middleware functions:

async function dbConnBeforeware(req, res, next) {
    const mongoConnString = process.env.ALD_CONN_STRING;
    const mongoClient = new MongoClient(mongoConnString);

    await mongoClient.connect();
    console.log("Database connection established!");

    req.dbClient = mongoClient;
    req.dbDatabaseRef = mongoClient.db('audit-log-demo');

    next();
}

async function dbConnAfterware(req, res, next) {
    await req.dbClient.close();

    console.log("Database connection closed!");

    next();
}

To use them, we need to adjust the way the routes are defined to:

async function getAllProfilesHandler(req, res, next) {
    try {
        const col = req.dbDatabaseRef.collection('user-profile');

        const profileList = await col.find({}).toArray();

        res.send({
            data: profileList
        });

        next();
    } catch (err) {
        next(err);
    }
}

async function getProfileByIdHandler(req, res, next) {
    try {
        const col = req.dbDatabaseRef.collection('user-profile');

        const profile = await col.findOne({ _id: ObjectId(req.params.id) });

        res.send({
            data: profile
        });

        next();
    } catch (err) {
        next(err);
    }
}

// For readability, we also created 2 new separate functions for the actual request handlers
expressApp.get('/profiles', dbConnBeforeware, getAllProfilesHandler, dbConnAfterware);
expressApp.get('/profile/:id', dbConnBeforeware, getProfileByIdHandler, dbConnAfterware);

Checkpoint

You can check out the index.js code at this point by clicking here.

Handling errors

Another point of improvement with the current code is error handling.

If something goes wrong in the request handlers, the default Express error handler will be triggered. But, this default error handler does not close the database connection established.

To fix this, we introduce our very own error handler by adding this code after the route definition section:

expressApp.use(async function (err, req, res, next) {
    if (req.dbClient) {
        await req.dbClient.close();
        console.log("Database connection closed!");
    }

    console.error(err.stack);
    res.status(500).send('Something broke!');
});

In this custom error handler, we close the connection if any, and then log the error to the console. Lastly, we inform the API consumer that something went wrong.

Now, when an error occurs, you should get this response (Postman screenshot): Error Handling Response.PNG

Checkpoint

You can check out the index.js code at this point by clicking here.

At this point, I added a forced error to the getProfileByIdHandler handler to simulate an error happening.

To view the version of the code without any of the forced errors, click here.

In Summary

We've successfully created an API built on Express and MongoDB!

Additionally, we've also gone through 2 rounds of code optimizations:

  1. Middleware usage - for reducing code redundancy
  2. Custom error handling - for ensuring the database connection is closed even when issues occur

I think there are still a couple of improvements on this:

  1. Once your API grows, you should split the route definitions into multiple code files.
  2. The dbConnBeforeware can also be made configurable so you can use it for other routes that handle data from another collection.

What other improvements do you have in mind? And what do you think of this approach? Let me know your thoughts in the comments

Glad that you've reached the end of this post. I hoped you learned something new from me today.


Hey, you! Follow me on Twitter!

 
Share this