Guide for Mongoose ODM (2025-2026 Edition), covering Mongoose 8.x/9.x, TypeScript integration, and performance best practices.
This skill provides modern guidelines for using Mongoose with MongoDB, focusing on Mongoose 8.x/9.x, strict TypeScript integration, and performance optimizations relevant to the 2025 ecosystem.
@types/mongoose is obsolete.async/await iterators and native Promises.Do NOT extend LengthyDocument or standard Document. Use a plain interface and let Mongoose infer the rest.
Define what your data looks like in plain JavaScript objects.
import { Types } from 'mongoose';
export interface IUser {
name: string;
email: string;
role: 'admin' | 'user';
tags: string[];
organization?: Types.ObjectId; // Use specific type for ObjectIds
createdAt?: Date;
}
Create the model with generic typing.
import mongoose, { Schema, model } from 'mongoose';
import { IUser } from './interfaces';
const userSchema = new Schema<IUser>({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
role: { type: String, enum: ['admin', 'user'], default: 'user' },
tags: [String],
organization: { type: Schema.Types.ObjectId, ref: 'Organization' }
}, {
timestamps: true /* Automatically manages createdAt/updatedAt */
});
// ⚡ 2025 Best Practice: Avoid "extends Document" on the interface.
// Let 'model<IUser>' handle the HydratedDocument type automatically.
export const User = mongoose.models.User || model<IUser>('User', userSchema);
// 'user' is typed as HydratedDocument<IUser> automatically
const user = await User.findOne({ email: 'test@example.com' });
if (user) {
console.log(user.name); // Typed string
await user.save(); // Typed method
}
.lean() for ReadsAlways use .lean() for read-only operations (e.g., GET requests). It skips Mongoose hydration, speeding up queries by 30-50%.
// Returns plain JS object (IUser), NOT a Mongoose document
const users = await User.find({ role: 'admin' }).lean();
Define compound indexes for common query patterns directly in the schema.
// Schema definition
userSchema.index({ organization: 1, email: 1 }); // Optimized lookup
Fetch only what you need. Network I/O is often the bottleneck.
const names = await User.find({}, { name: 1 }).lean();
For modern serverless/edge environments (like Vercel, Next.js, Remix), use a singleton pattern with caching to prevent connection exhaustion.
import mongoose from 'mongoose';
const MONGODB_URI = process.env.MONGODB_URI!;
if (!MONGODB_URI) {
throw new Error('Please define the MONGODB_URI environment variable');
}
/**
* Global is used here to maintain a cached connection across hot reloads
* in development. This prevents connections growing exponentially
* during API Route usage.
*/
let cached = (global as any).mongoose;
if (!cached) {
cached = (global as any).mongoose = { conn: null, promise: null };
}
async function connectDb() {
if (cached.conn) {
return cached.conn;
}
if (!cached.promise) {
const opts = {
bufferCommands: false,
};
cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
return mongoose;
});
}
try {
cached.conn = await cached.promise;
} catch (e) {
cached.promise = null;
throw e;
}
return cached.conn;
}
export default connectDb;