Most MongoDB + Next.js tutorials show you the basics but skip the production problems.
Connection leaks. Multiple connections in development. No error handling.
Here's how to do it correctly.
The Problem With Basic Connection Code
Most tutorials show this:
import mongoose from 'mongoose';
export async function connectDB() {
await mongoose.connect(process.env.MONGODB_URI!);
}
This works — until it doesn't.
In Next.js development mode, hot reloading creates a new connection every time a file changes. After 10 minutes you have 50 open connections and MongoDB starts refusing them.
The Correct Connection Pattern
// lib/db.ts
import mongoose from 'mongoose';
const MONGODB_URI = process.env.MONGODB_URI!;
if (!MONGODB_URI) {
throw new Error('MONGODB_URI is not defined in environment variables');
}
// Cache connection across hot reloads in development
let cached = global.mongoose as {
conn: typeof mongoose | null;
promise: Promise<typeof mongoose> | null;
};
if (!cached) {
cached = global.mongoose = { conn: null, promise: null };
}
export async function connectDB() {
// Return cached connection if exists
if (cached.conn) {
return cached.conn;
}
// Create new connection if no promise exists
if (!cached.promise) {
cached.promise = mongoose.connect(MONGODB_URI, {
bufferCommands: false,
}).then((mongoose) => mongoose);
}
try {
cached.conn = await cached.promise;
} catch (error) {
cached.promise = null;
throw error;
}
return cached.conn;
}
The global.mongoose cache persists across hot reloads in development. In production it runs once and reuses the connection.
TypeScript Fix for Global Cache
Add this to your types/global.d.ts:
// types/global.d.ts
import mongoose from 'mongoose';
declare global {
var mongoose: {
conn: typeof mongoose | null;
promise: Promise<typeof mongoose> | null;
};
}
Without this TypeScript will complain about global.mongoose.
Creating Models Correctly
Another common mistake — defining models without checking if they already exist:
// 🚫 This throws an error on hot reload
const User = mongoose.model('User', UserSchema);
// ✅ Check if model exists first
const User = mongoose.models.User || mongoose.model('User', UserSchema);
Full model example:
// models/User.ts
import mongoose, { Document, Model } from 'mongoose';
export interface IUser extends Document {
name: string;
email: string;
role: 'admin' | 'user';
createdAt: Date;
}
const UserSchema = new mongoose.Schema<IUser>(
{
name: {
type: String,
required: [true, 'Name is required'],
trim: true,
minlength: 2,
maxlength: 50,
},
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
trim: true,
},
role: {
type: String,
enum: ['admin', 'user'],
default: 'user',
},
},
{
timestamps: true, // adds createdAt and updatedAt automatically
}
);
// Index for faster email lookups
UserSchema.index({ email: 1 });
const User: Model<IUser> =
mongoose.models.User || mongoose.model<IUser>('User', UserSchema);
export default User;
Using it in API Routes
// app/api/users/route.ts
import { NextRequest } from 'next/server';
import { connectDB } from '@/lib/db';
import User from '@/models/User';
export async function GET() {
try {
await connectDB();
const users = await User.find({})
.select('-__v') // exclude version key
.sort({ createdAt: -1 })
.limit(50);
return Response.json({ success: true, data: users });
} catch (error) {
console.error('GET /api/users error:', error);
return Response.json(
{ success: false, error: 'Failed to fetch users' },
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
await connectDB();
const body = await request.json();
const user = await User.create(body);
return Response.json(
{ success: true, data: user },
{ status: 201 }
);
} catch (error: any) {
// Handle duplicate email error
if (error.code === 11000) {
return Response.json(
{ success: false, error: 'Email already exists' },
{ status: 409 }
);
}
return Response.json(
{ success: false, error: 'Failed to create user' },
{ status: 500 }
);
}
}
The error.code === 11000 check catches MongoDB duplicate key errors — common when you have unique indexes.
Environment Variables
# .env.local
MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/dbname?retryWrites=true&w=majority
Never commit this file. Add .env.local to your .gitignore.
Common Errors and Fixes
| Error | Cause | Fix |
|-------|-------|-----|
| buffering timed out | No connection before query | Always call connectDB() first |
| Cannot overwrite model | Model defined multiple times | Use mongoose.models.X \|\| mongoose.model() |
| MongoServerError: E11000 | Duplicate unique field | Handle error.code === 11000 |
| Too many connections | No connection caching | Use the global cache pattern above |
I use this exact pattern in my NeuroDash SaaS dashboard template.
See it live: https://neurodash-dashbord.vercel.app/
Get the template: https://pixelanas.gumroad.com/l/neuro-dash
Questions about MongoDB + Next.js? Drop them below 👇
Anas — full-stack Next.js developer. Building SaaS products and premium templates. X: @pixelanas