Building an API using Express and MongoDB
Tags: mongodb, api, express, node
Published: 2021-10-01 18:57:36

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](https://www.mongodb.com/cloud/atlas).
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](https://www.mongodb.com/products/compass).
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6eicd5iojved19kugsqy.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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l2xiw7dl44vf3uu5pyfw.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:
        
        ```json
        "firstName": "Tony",
        "lastName": "Stark",
        "age": 25   
        ```
        ![DB 1.2.8.4 Collection List](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3o4n1i9m6izsx5kwn7nr.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:
    
    ```jsx
    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](https://www.postman.com/)), access the `/profiles` endpoint of your API. You should get this result:
    
    ```json
    {
        "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:

```jsx
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](https://github.com/projectkenneth/express-mongodb-demo/tree/Initial-State).

## Optimizing your Express routes

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

```jsx
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:

```jsx
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:

```jsx
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](https://github.com/projectkenneth/express-mongodb-demo/blob/Introducing-Middlewares/index.js).

## 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:

```jsx
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gabiypa7j5nzikgigykk.PNG) 

### Checkpoint

You can check out the `index.js` code at this point by clicking [here](https://github.com/projectkenneth/express-mongodb-demo/blob/Introduce-Error-Handling/index.js).

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](https://github.com/projectkenneth/express-mongodb-demo/blob/master/index.js).

## 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](https://twitter.com/projectkenneth)!