Ultimate Guide to MongoDB Connections in Next.js: Edge and Serverless Safety

When building with Next.js, especially when using API routes or server actions, it's important to manage database connections carefully. Unlike traditional servers, Next.js often operates in serverless or edge environments, where functions start and stop quickly.

In "edge" conditions, you can't just connect to an external entity (like your MongoDB database) once and be done with it. Next.js is a framework with its own rules. It's not like a typical Node.js Express server with an MVC architecture where you connect to external entities just once. That kind of server is different; it starts and runs on the same cloud or host wherever you're operating it.

Next.js is different because it runs in an "edge" runtime. What does this mean? Your JavaScript runs closer to the user, on Edge servers, not on a typical central Node.js server.

This "edge" can be anywhere—in your country, maybe in the USA, or somewhere else. Even if it’s nearby, there could still be multiple "edges" running at the same time to provide the fastest response.

So, if edges make our Next.js runtime fast and responsive, why are we concerned about database connections? Wouldn't they be faster too?

The answer is NO! Think of it this way: Let’s say there are 5 edges in your city. I send an API request to your Next.js app to fetch something from the database and return it to me—a simple request-response cycle, right? This request goes to one of the 5 edges, where your database connection is made, and everything works fine. Now, if I make the same request again, what do you think will happen? If you think you'll be connected via the same edge, you're wrong, as it can be anywhere, right?

Now, let's say the request is successful even on a different edge. Imagine 100 such requests— would you want 100 edges to make the database connection 100 times? Wouldn't that be costly in production in terms of resources and connection pools?

So, what is the best approach to avoid this? Just make a connection once and ensure you check on every API call if your database is already connected or not.

✅ The Solution: Caching the Connection

Yes, caching is the simplest solution. In Next.js, our API routes can run on a Node.js runtime unless we specifically set them to run on the edge. Here, we can safely cache the MongoDB connection using global.
We just need to create a mechanism where, if the connection is being made for the first time (not cached in global), we establish it using our MongoDB URI. Otherwise, we simply retrieve it from global and return it.

Steps to cache the MongoDB connection in Next.js

Step 1: Define your MONGO_URL in your .env file.

MONGODB_URL=mongodb://localhost:27017/your_database_name

Right now, I have written the url from local environment, you can add you own either from local or MongoAtlas instance.

Step 2: In your root, directory make a file called type.d.ts and write the following code:

/* eslint-disable no-var */
import { Connection } from "mongoose"

declare global {
    var mongoose: {
        conn: Connection | null
        promise: Promise<Connection> | null
    }
}


export {};

Why do we need this? Because mongoose isn't available in our global scope by default. So, we need to define it ourselves using TypeScript.

Here, conn stands for our established connection, which might or might not exist. Meanwhile, promise represents our connection in progress, which returns a promise. Both are used based on whether the connection already exists.

Step 3: Simply create a db.ts file in your util or lib folder, or wherever you keep your utility functions. Paste the code below into it:

import mongoose from "mongoose";

const MONGODB_URL = process.env.MONGODB_URL!;

if (!MONGODB_URL) {
    throw new Error("Please define the MONGODB_URL environment variable inside env file");
}

let cached = global.mongoose;

if (!cached) {
    cached = global.mongoose = {
        conn: null,
        promise: null
    };
}

export async function connectToDatabase() {
    if (cached.conn) {
        console.log("Using cached database connection.");
        return cached.conn;
    }

    if (!cached.promise) {
        const options = {
            bufferCommands: true,  
//controls whether Mongoose should queue 
//(buffer) operations (like .save(), .find(), etc.)
// until a connection is established
            maxPoolSize: 10, 
// depends on mongodb plan bought (max number of connections)
        };

        cached.promise = mongoose
            .connect(MONGODB_URL, options)
            .then(() => {
                console.log("Database connection successful.");
                return mongoose.connection;
            });
    }

    try {
        cached.conn = await cached.promise;
        return cached.conn;
    } catch (error) {
        cached.promise = null;
        console.error("Database connection failed:", error);
        throw new Error("Database connection failed: " + error);
    }
}

Let's go over this code—

First, in the section below, we import mongoose ORM to manage our database operations and then retrieve our MONGODB_URL from the environment.

import mongoose from "mongoose";

const MONGODB_URL = process.env.MONGODB_URL!;

if (!MONGODB_URL) {
    throw new Error("Please define the MONGODB_URL environment variable inside env file");
}

Next, in the following part, we extract our cached connection, which might or might not exist, using the global object. If it is not present, we create it safely since we have already defined the types.

let cached = global.mongoose;

if (!cached) {
    cached = global.mongoose = {
        conn: null,
        promise: null
    };
}

In the remaining section, we define our database connection function. We first check if a connection has already been made by verifying the existence of cached.conn. If it exists, we return it. If not, and if cached.promise does not exist, it means there is no ongoing connection attempt. So, we use mongoose.connect to connect to our database with our chosen options. Finally, in the try-catch block, we wait for the promise to complete and then store our connection in the conn variable.

Voila, just like that you have now set up a perfect database connection code for your Next.js project.

Now, whenever you need to perform a database operation in your Next.js backend, just call this function connectToDatabase() once, and you won't have any issues with your database-related tasks.

That’s it. If you have any feedback or query or if you think I missed something. Kindly mention in comments. Thank you for reading😁!!

0
Subscribe to my newsletter

Read articles from AYUSH KUMAR GUPTA directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

AYUSH KUMAR GUPTA
AYUSH KUMAR GUPTA

I'm a college student specializing in Full Stack Web Development, with a solid grasp of programming fundamentals and expertise in C,C++, Java, JavaScript and Python. Passionate about crafting seamless digital experiences, I'm on a quest to understand the intricacies of web development from backend to frontend. Eager to connect with individuals and organizations that share a zeal for innovation and learning in tech