Sharing a MongoDB connection in Node.js/Express

In this article, we'll discuss how to share a MongoDB connection in a Node.js/Express application.

Developers often make the mistake of creating a RESTful API using the previously mentioned technologies but do not dig deeper to see if their implementation is performing well or not. Often, the mistake puts a strain on the database especially if the application grows rapidly and there are an increased number of requests that hit the database.

Incorrect implementation

Let's first take a look at an example of an incorrect implementation.

const express = require('express');
const app = express();
const MongoClient = require('mongodb').MongoClient;
const ObjectId = require('mongodb').ObjectId;

const port = 3000;

const mongo_uri = 'mongodb://localhost:32768';

app.get('/', (req, res) => {
  MongoClient.connect(mongo_uri, { useNewUrlParser: true })
  .then(client => {
    const db = client.db('my-db');
    const collection = db.collection('my-collection');
    collection.find({}).toArray().then(response => res.status(200).json(response)).catch(error => console.error(error));
  });
});

app.get('/:id', (req, res) => {
  const id = new ObjectId(req.params.id);
  MongoClient.connect(mongo_uri, { useNewUrlParser: true })
  .then(client => {
    const db = client.db('my-db');
    const collection = db.collection('my-collection');
    collection.findOne({ _id: id }).then(response => res.status(200).json(response)).catch(error => console.error(error));
  });
});

app.listen(port, () => console.info(`REST API running on port ${port}`));

In the code above, we create a bunch of initial variables and define two API endpoints. Each endpoint connects to the database and executes a simple query.

The issue with this approach is that each time we call an endpoint a connection is going to be created and that's not the best thing to do as it will not scale very well.

We can verify that a new connection is made by starting up the API and making a request against both of the endpoints and take a look at the number of active connections in MongoDB (via its CLI):

> db.serverStatus().connections;
{ "current" : 3, "available" : 838857, "totalCreated" : 37 }

We have 3 connections - 2 for the API and 1 for the CLI itself that connects to the database. This is not a right approach since ideally, we'd like to share one connection throughout the entire application.

Sharing the connection

There are a bunch of approaches that we can follow, and we'll discuss one that seems to be a really interesting one. We'll base our application on the fact that the API should not be available if the database is not available that powers it. This makes sense - there's no point in providing any endpoints if the database is down and we can't effectively display data.

To achieve this, we need to re-think our logic around connecting to the database a little bit - first, we should attempt to make the connection, and if that is successful, we can fire up the API server as well.

MongoClient.connect(mongo_uri, { useNewUrlParser: true })
.then(client => {
  const db = client.db('my-db');
  const collection = db.collection('my-collection');
  app.listen(port, () => console.info(`REST API running on port ${port}`));
}).catch(error => console.error(error));

So far so good but now the question is, how do we make the connection available. The thing is, we don't need to. It's enough if we provide the routes with the collection information.

Express has a great feature to share data between routes (especially useful with modular code). This is a simple object app.locals, and we can attach properties to it and access it later on inside the route definitions:

// add this line before app.listen()
app.locals.collection = collection;

Once we have the collection information we can rework our routes as well:

app.get('/', (req, res) => {
  const collection = req.app.locals.collection;
  collection.find({}).toArray().then(response => res.status(200).json(response)).catch(error => console.error(error));
});

app.get('/:id', (req, res) => {
  const collection = req.app.locals.collection;
  const id = new ObjectId(req.params.id); 
  collection.findOne({ _id: id }).then(response => res.status(200).json(response)).catch(error => console.error(error));
});

Moreover, if we now check the database connection count we'll see only 2 active connections:

> db.serverStatus().connections;
{ "current" : 2, "available" : 838858, "totalCreated" : 39 }

One for our API and one for the CLI. This connection count will never increase since we connect to the database once when we start up the API itself, and that's it.

Connection Pooling

Now, of course, there are situations where the API not only returns information from a database but some other piece of data as well - in this case, we may not necessarily want to depend on the fact whether the database is up or down - only the endpoints where a database query is made should fail.

Please note that by default the MongoDB Node.js API has a connection pool of 5 connections.

Let's take a look at an example where we will extend the connection pool as well as make sure that our API is capable of returning static data should the database be down.

// code excerpt
let db;
let collection;

MongoClient
  .connect(mongo_uri, { useNewUrlParser: true, poolSize: 10 })
  .then(client => {
    db = client.db('my-db');
    collection = db.collection('my-collection');
  })
  .catch(error => console.error(error));

app.get('/static', (req, res) => {
  res.status(200).json('Some static data')
});

app.get('/', (req, res) => {
  collection.find({}).toArray().then(response => res.status(200).json(response)).catch(error => console.error(error));
});

Again, checking the connection count should display 2 connections.

Database disconnect

It is recommended not to close database connections via Node.js. There are multiple reasons behind this thought. First of all, if we need to use different databases, we can use the a db() method to select another database and still use the pool of connections. This pool is there to make sure that a blocking operation cannot freeze the Node.js process/application.

Of course, it'd make sense to disconnect from the database if the application closes. We can achieve this using the following few lines of code:

// create an initial variable
let dbClient;
// assign the client from MongoClient
MongoClient
  .connect(mongo_uri, { useNewUrlParser: true, poolSize: 10 })
  .then(client => {
    db = client.db('my-db');
    dbClient = client;
    collection = db.collection('my-collection');
  })
  .catch(error => console.error(error));
// listen for the signal interruption (ctrl-c)
process.on('SIGINT', () => {
  dbClient.close();
  process.exit();
});

Conclusion

In this article, we have seen ways to handle MongoDB connections in a Node.js/Express application. Remember to be careful to manage the connections to avoid slowing down the application via exhaustive memory consumption.