14
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:
- Setting up MongoDB
- Creating an Express application
- Optimizing your Express routes
- Handling errors
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.
- Create a MongoDB Atlas account here.
- Create a cluster. For new accounts, you can use the forever free tier!
- Create the super admin user.
To better visualize our data, we'll be using the official GUI for MongoDB, Compass.
- Download the latest Compass version here.
- Install the thing!
- Get the database connection string from Atlas.
- 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.
- Launch Compass, key in your connection string, then, click Connect.
- Once connected, you can now click on the Create Database button.
- Specify the database name and the first collection's name. Then, click the Create Database button on the popup.
- For this example, I created a database named
audit-log-demo
and a collection nameduser-profile
.
- For this example, I created a database named
- You should now see
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
- Click on the
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.
- Open your favorite CLI and navigate to your desired working directory.
- Create a new package using
npm init
. Follow the prompts and provide the necessary details. - Install both
express
and themongodb
driver by executingnpm install mongodb express --save
- Get the database's connection string from Atlas.
- 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.
- Create a new environment setting with key
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 theuser-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:
- A middleware that establishes the connection to the database and will then pass this connection instance to the request handlers.
- A middleware that closes the connection to the database. This middleware function will be executed after the request handlers.
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:
- Middleware usage - for reducing code redundancy
- 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:
- Once your API grows, you should split the route definitions into multiple code files.
- 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!
14