The proper way to connect to MongoDB in NextJS (handling connection failure)

March 15, 2022

If you've ever used MongoDB with NextJS, you may recognise the following code from the official Next/Mongo example project template.

What you may not realise is that this example code will cause you a headache at some point.

import { MongoClient } from 'mongodb';
 
const uri = process.env.MONGODB_URI;
const options = {};
 
let client
let clientPromise
 
if (!process.env.MONGODB_URI) {
  throw new Error('Please add your Mongo URI to .env.local')
}
 
if (process.env.NODE_ENV === 'development') {
  // In development mode, use a global variable so that the value
  // is preserved across module reloads caused by HMR (Hot Module Replacement).
  if (!global._mongoClientPromise) {
    client = new MongoClient(uri, options)
    global._mongoClientPromise = client.connect()
  }
  clientPromise = global._mongoClientPromise
} else {
  // In production mode, it's best to not use a global variable.
  client = new MongoClient(uri, options)
  clientPromise = client.connect()
}
 
// Export a module-scoped MongoClient promise. By doing this in a
// separate module, the client can be shared across functions.
export default clientPromise

The problem: Server restarts can break your site.

If the Mongo server is ready before the Next / Express site, everything will be just fine.

In a nutshell, if the server restarts and the Next app comes up before Mongo is ready, the connection attempt will fail and the promise will reject. This will semi-permanently break your database connection, disabling your site until the Next app restarted.

Why does this happen?

If you come from a PHP background (or similar) you'll be used to each request typically spawning a completely new instance of server-side code, including a new database connection. However with node-based servers, the singleton database connection persists across requests as node servers typically run as a daemon.

How can I fix it?

We can use a library called promise-retry which will detect the rejected promise, then reattempt the connection.

import { MongoClient } from 'mongodb';
import promiseRetry from 'promise-retry';
 
const uri = process.env.MONGODB_URI || '';
const options = {}
const promiseRetryOptions = {
  retries: 10,
  factor: 1, // sets the factor for exponential backoff
  minTimeout: 1000,
  maxTimeout: 2000
}
let client;
let clientPromise;
 
if (!process.env.MONGODB_URI) {
  throw new Error('Please add your Mongo URI to .env.local')
}
 
if (process.env.NODE_ENV === 'development') {
 
  // In development mode, use a global variable so that the value
  // is preserved across module reloads caused by HMR (Hot Module Replacement).
  if (!global._mongoClientPromise) {
 
    client = new MongoClient(uri, options)
    global._mongoClientPromise = promiseRetry((retry, number) => {
      console.log(`Mongo (dev) connection attempt number: ${number}`);
      return client.connect().catch(retry)
    }, promiseRetryOptions).then((promise) => {
      console.log('Mongo (dev) successfully connected.');
      return promise;
    });
 
  }
  clientPromise = global._mongoClientPromise
 
} else {
  // In production mode, it's best to not use a global variable.
  client = new MongoClient(uri, options)
  clientPromise = promiseRetry((retry, number) => {
 
    console.log(`Mongo (prod) connection attempt number: ${number}`)
    return client.connect().catch(retry)
  
  }, promiseRetryOptions).then((promise) => {
    console.log('Mongo (prod) successfully connected.');
    return promise;
  });
}
 
// Export a module-scoped MongoClient promise. By doing this in a
// separate module, the client can be shared across functions.
export default clientPromise

Luke Taylor is a software developer specialising in web. He mostly works with Next.js but has experience with a range of languages and frameworks. Based in London, UK.