22
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:
Let's start!
In this section, we'll go through creating and configuring a remote MongoDB instance.
Instead of installing a local MongoDB instance, we'll use Atlas which is MongoDB's official database-as-a-service.
To better visualize our data, we'll be using the official GUI for MongoDB, Compass.
- Access your Atlas dashboard. Then, on your cluster panel, click the Connect button.
- On the Connect popup, create your super admin user.
- Then, for the Connection Method, choose Connect using MongoDB Compass.
- Then, choose the latest Compass version and then copy the connection string.
- Replace the credentials in the connection string with your actual credentials.
- Keep the connection string somewhere safe so you can use it in the next steps.
- For this example, I created a database named
audit-log-demo
and a collection nameduser-profile
.
audit-log-demo
as part of the database list.Now, let's add test data to our database.
- Click on the
audit-log-demo
database. You will be directed to the collection list page. - Click on the
user-profile
collection. You will be directed to the collection management page. - Under the Documents tab, click on the Add Data > Insert Document button.
-
In the Insert to Collection popup, paste the following properties just below the _id property:
"firstName": "Tony", "lastName": "Stark", "age": 25
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.
npm init
. Follow the prompts and provide the necessary details.express
and the mongodb
driver by executing npm install mongodb express --save
- Access your Atlas dashboard. Then, on your cluster panel, click the Connect button.
- Then, for the Connection Method, choose Connect your application.
- Then, choose the appropriate NodeJS version and then copy the connection string.
- Replace the credentials in the connection string with your actual credentials.
- Keep the connection string somewhere safe so you can use it in the next steps.
ALD_CONN_STRING
and set its value to your connection string.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.
Run your application by executing node index.js
on your CLI.
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
}
]
}
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();
}
});
You can check out the
index.js
code at this point by clicking here.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!
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:
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);
You can check out the
index.js
code at this point by clicking here.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.
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.
We've successfully created an API built on Express and MongoDB!
Additionally, we've also gone through 2 rounds of code optimizations:
I think there are still a couple of improvements on this:
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!
22