Adding MongoDB to a Next.js App (App Router)

MongoDB Atlas with Mongoose

Based on the rackit codebase.


Prerequisites

  • Node.js 18+
  • A running MongoDB instance — one of:
  • A Next.js 13+ project using the App Router (src/app/)
  • TypeScript (recommended)

Step 1 — Install dependencies

Two packages are needed:

  • mongodb — the official MongoDB driver (used by NextAuth adapter and for direct client access)
  • mongoose — ODM for schema-based model definitions
1
npm install mongodb mongoose

If using NextAuth.js with a MongoDB adapter:

1
npm install next-auth @auth/mongodb-adapter

Step 2 — Set the environment variable

Add to .env.local:

1
MONGODB_URI=mongodb+srv://<user>:<password>@cluster0.xxxxx.mongodb.net/<dbname>?retryWrites=true&w=majority

For local MongoDB:

1
MONGODB_URI=mongodb://localhost:27017/myapp

Never commit .env.local to source control. Add it to .gitignore.


Step 3 — Create the MongoClient helper (src/lib/mongodb.ts)

This file creates a singleton MongoClient promise, reusing the connection across hot reloads in development and across requests in production.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { MongoClient } from "mongodb";

const uri = process.env.MONGODB_URI!;

if (!uri) {
throw new Error("MONGODB_URI environment variable is not set");
}

declare global {
var _mongoClientPromise: Promise<MongoClient> | undefined;
}

let clientPromise: Promise<MongoClient>;

if (process.env.NODE_ENV === "development") {
if (!global._mongoClientPromise) {
global._mongoClientPromise = new MongoClient(uri).connect();
}
clientPromise = global._mongoClientPromise;
} else {
clientPromise = new MongoClient(uri).connect();
}

export default clientPromise;

Used by: NextAuth’s MongoDBAdapter.


Step 4 — Create the Mongoose connection helper (src/lib/db.ts)

Lazy initialisation — only connects if not already connected. Safe to call from any server action or API route.

1
2
3
4
5
6
7
8
9
10
11
12
import mongoose from "mongoose";

const MONGODB_URI = process.env.MONGODB_URI!;

if (!MONGODB_URI) {
throw new Error("MONGODB_URI environment variable is not set");
}

export async function connectDB() {
if (mongoose.connection.readyState >= 1) return;
await mongoose.connect(MONGODB_URI);
}

Used by: Mongoose models and server actions.


Step 5 — Define Mongoose models (src/models/)

Each model follows this pattern to avoid re-registering on hot reload:

1
2
3
4
5
6
7
8
9
10
11
12
13
import mongoose, { Schema } from "mongoose";

const UserSchema = new Schema(
{
email: { type: String, required: true, unique: true, lowercase: true, trim: true },
passwordHash: { type: String, required: true },
displayName: { type: String, required: true, trim: true },
},
{ timestamps: { createdAt: true, updatedAt: false } }
);

export const User =
mongoose.models.User || mongoose.model("User", UserSchema);

Key conventions:

  • Always guard with mongoose.models.ModelName || mongoose.model(...) to prevent “Cannot overwrite model” errors on hot reload.
  • Place all models under src/models/.
  • Use timestamps option for automatic createdAt/updatedAt fields.

Step 6 — Use the DB in Server Actions or Route Handlers

1
2
3
4
5
6
7
import { connectDB } from "@/lib/db";
import { User } from "@/models/user";

export async function getUserByEmail(email: string) {
await connectDB();
return User.findOne({ email: email.toLowerCase() });
}

Always call connectDB() before any Mongoose query.


Step 7 — (Optional) Wire up NextAuth MongoDB adapter

If using NextAuth.js for authentication, pass the clientPromise from Step 3 to the adapter. This stores sessions, accounts, and verification tokens in MongoDB automatically.

1
2
3
4
5
6
7
8
9
import NextAuth from "next-auth";
import { MongoDBAdapter } from "@auth/mongodb-adapter";
import clientPromise from "@/lib/mongodb";

export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: MongoDBAdapter(clientPromise),
session: { strategy: "jwt" },
// ... providers
});

Summary of files created

File Purpose
src/lib/mongodb.ts Singleton MongoClient promise for the raw driver / NextAuth adapter
src/lib/db.ts Lazy Mongoose connection helper called before every query
src/models/*.ts Mongoose schema + model definitions (one file per collection)
.env.local MONGODB_URI connection string (not committed)

Common pitfalls

Problem Fix
“Cannot overwrite model once compiled” Guard all models with mongoose.models.X || mongoose.model(...)
Connection spam in development Use global._mongoClientPromise to survive hot reloads
MONGODB_URI undefined at runtime Check .env.local exists and Next.js was restarted after adding it
Mongoose not connected before query Always await connectDB() at the top of every server action
Storing secrets in source control Keep .env.local in .gitignore; use Vercel env vars in production