1. Introduction đą
Mongoose is an ODM library for MongoDB and Node.js that provides a straightforward, schema-based solution to model your application data. It wraps the native MongoDB driver with structure, validation, and convenience utilities.
In this tutorial, you'll learn how to define schemas and models, work with documents, apply validation and middleware, handle relationships via population, and optimize Mongoose for production use.
Information
2. What is Mongoose? đ§Š
Mongoose sits on top of the native MongoDB driver and introduces a schema-based approach to modeling data, even though MongoDB itself is schema-less by design.
- Schemas define the shape and rules of your documents.
- Models are constructors compiled from schemas, used to interact with collections.
- Middleware (hooks) let you run logic before/after certain operations.
3. Why Use Mongoose? đ¤
| Feature | Native Driver | Mongoose |
|---|---|---|
| Schema enforcement | â | â |
| Built-in validation | â | â |
| Middleware/hooks | â | â |
| Population (joins) | Manual | â Built-in |
| Raw performance | Slightly faster | Small overhead |
Best Practice
4. Installing Mongoose đĻ
terminal
npm install mongooseversion-check.js
const mongoose = require('mongoose');
console.log(mongoose.version);Tip
5. Connecting to MongoDB đ
connect.js
const mongoose = require('mongoose');
async function connectDB() {
try {
await mongoose.connect(process.env.MONGO_URI, {
dbName: 'shopDB',
});
console.log('MongoDB connected successfully! â
');
} catch (err) {
console.error('Connection error â:', err);
process.exit(1);
}
}
module.exports = connectDB;Connection Events
connection-events.js
mongoose.connection.on('connected', () => console.log('Mongoose connected đ'));
mongoose.connection.on('error', (err) => console.error('Mongoose error:', err));
mongoose.connection.on('disconnected', () => console.log('Mongoose disconnected đ'));Important
6. Schemas đ
A Schema defines the structure, field types, and rules for documents within a collection.
user-schema.js
const { Schema } = require('mongoose');
const userSchema = new Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
age: { type: Number, min: 0 },
createdAt: { type: Date, default: Date.now },
});Note
7. Models đī¸
A Model is compiled from a schema and provides the interface for creating, querying, updating, and deleting documents in the corresponding collection.
user-model.js
const mongoose = require('mongoose');
const userSchema = require('./userSchema');
const User = mongoose.model('User', userSchema);
module.exports = User;Tip
8. Documents đ
A document is an instance of a model, representing a single record with built-in methods for saving, validating, and manipulating itself.
documents.js
// Create a new document
const alice = new User({ name: 'Alice', email: 'alice@example.com', age: 28 });
await alice.save();
// Query for documents
const user = await User.findOne({ name: 'Alice' });
console.log(user.name, user._id);
// Update and save
user.age = 29;
await user.save();- document.isNew â true if the document hasn't been saved yet.
- document.isModified('field')â checks whether a specific field has changed.
- document.toObject() / document.toJSON()â converts to a plain object.
9. Data Types đ¤
| Type | Example |
|---|---|
| String | name: String |
| Number | age: Number |
| Boolean | isActive: Boolean |
| Date | createdAt: Date |
| Array | tags: [String] |
| ObjectId | author: Schema.Types.ObjectId |
| Mixed | metadata: Schema.Types.Mixed |
| Map | settings: Map |
data-types.js
const productSchema = new Schema({
title: String,
price: Number,
inStock: Boolean,
tags: [String],
category: Schema.Types.ObjectId,
metadata: Schema.Types.Mixed,
});10. Schema Options âī¸
schema-options.js
const postSchema = new Schema(
{
title: String,
body: String,
},
{
timestamps: true, // adds createdAt & updatedAt
versionKey: false, // disables __v field
collection: 'blogPosts' // custom collection name
}
);- timestamps â automatically manages createdAt and updatedAt.
- versionKey â toggles the internal __v version field.
- collectionâ overrides the default pluralized collection name.
11. Validation â
Mongoose runs validation automatically before saving a document, based on rules defined in the schema.
validation.js
const productSchema = new Schema({
title: { type: String, required: [true, 'Title is required'] },
price: { type: Number, min: [0, 'Price cannot be negative'] },
category: {
type: String,
enum: ['electronics', 'clothing', 'food'],
},
email: {
type: String,
validate: {
validator: (v) => /^\S+@\S+\.\S+$/.test(v),
message: (props) => `${props.value} is not a valid email!`,
},
},
});handle-validation-error.js
try {
await new Product({ price: -10 }).save();
} catch (err) {
if (err.name === 'ValidationError') {
console.error('Validation failed:', err.errors);
}
}Warning
12. Middleware đĒ
Mongoose middleware (also called hooks) are functions that execute at specific points during a document or query's lifecycle: pre or post.
middleware.js
userSchema.pre('save', async function (next) {
if (!this.isModified('password')) return next();
this.password = await hashPassword(this.password);
next();
});
userSchema.post('save', function (doc) {
console.log(`New user saved: ${doc.name} đ`);
});Reference
13. Virtuals đģ
Virtuals are computed properties that are not persisted to the database but are derived from existing fields.
virtuals.js
userSchema.virtual('fullName').get(function () {
return `${this.firstName} ${this.lastName}`;
});
userSchema.virtual('fullName').set(function (value) {
const [firstName, lastName] = value.split(' ');
this.firstName = firstName;
this.lastName = lastName;
});Tip
14. Instance Methods đ§
Instance methods are custom functions attached to individual documents via the schema's methods object.
instance-methods.js
userSchema.methods.comparePassword = function (candidatePassword) {
return bcrypt.compare(candidatePassword, this.password);
};
// Usage
const user = await User.findOne({ email });
const isMatch = await user.comparePassword('mypassword123');15. Static Methods đ
Static methods are attached to the model itself rather than individual documents, useful for model-level queries and utilities.
static-methods.js
userSchema.statics.findByEmail = function (email) {
return this.findOne({ email });
};
// Usage
const user = await User.findByEmail('alice@example.com');Note
16. Query Helpers đ
Query helpers extend Mongoose's chainable query builder with custom, reusable query logic.
query-helpers.js
userSchema.query.byActiveStatus = function () {
return this.where({ isActive: true });
};
// Usage â chainable!
const activeUsers = await User.find().byActiveStatus().sort({ name: 1 });17. Population đ
Population automatically replaces a referenced ObjectId with the actual document(s) it points to, emulating a SQL-style join.
population.js
const postSchema = new Schema({
title: String,
author: { type: Schema.Types.ObjectId, ref: 'User' },
});
const Post = mongoose.model('Post', postSchema);
// Populate the author field
const post = await Post.findOne({ title: 'Hello World' }).populate('author');
console.log(post.author.name);
// Populate with field selection
const posts = await Post.find().populate('author', 'name email');Warning
18. Indexes đ
indexes.js
userSchema.index({ email: 1 }, { unique: true });
userSchema.index({ age: 1, name: -1 }); // compound index
// Ensure indexes are built (useful in dev)
await User.syncIndexes();Best Practice
19. Transactions đ
Mongoose supports multi-document transactions via sessions, built atop MongoDB's native transaction API.
transactions.js
const session = await mongoose.startSession();
try {
await session.withTransaction(async () => {
const from = await Account.findOne({ _id: 'A' }).session(session);
const to = await Account.findOne({ _id: 'B' }).session(session);
from.balance -= 100;
to.balance += 100;
await from.save({ session });
await to.save({ session });
});
console.log('Transaction committed â
');
} finally {
session.endSession();
}Caution
20. Plugins đ
Plugins allow you to package reusable schema functionality (fields, methods, hooks) and apply it across multiple schemas.
plugins.js
function timestampPlugin(schema) {
schema.add({ createdBy: String, updatedBy: String });
schema.pre('save', function (next) {
this.updatedBy = 'system';
next();
});
}
userSchema.plugin(timestampPlugin);
postSchema.plugin(timestampPlugin);Example
21. Discriminators đ§Ŧ
Discriminatorsallow multiple related models to share a single underlying MongoDB collection while maintaining distinct schemas â a form of schema inheritance.
discriminators.js
const eventSchema = new Schema({ time: Date }, { discriminatorKey: 'kind' });
const Event = mongoose.model('Event', eventSchema);
const ClickedEvent = Event.discriminator(
'Clicked',
new Schema({ element: String })
);
const PurchasedEvent = Event.discriminator(
'Purchased',
new Schema({ amount: Number })
);Information
22. Error Handling â ī¸
error-handling.js
try {
await User.create({ email: 'duplicate@example.com' });
} catch (err) {
if (err.name === 'ValidationError') {
console.error('Validation error:', err.errors);
} else if (err.code === 11000) {
console.error('Duplicate key error đĢ:', err.keyValue);
} else {
console.error('Unexpected error:', err);
}
}Tip
23. Performance Optimization đ
Lean Queries
lean.js
// Returns plain JS objects instead of full Mongoose documents â faster for read-only data
const users = await User.find().lean();Field Selection
select.js
const users = await User.find().select('name email -_id');- Use .lean() when you don't need document methods or change tracking.
- Avoid populate() in hot paths; consider denormalization instead.
- Use .select() to limit returned fields.
- Batch writes using insertMany() instead of looping save() calls.
24. Best Practices â
- Keep schemas in separate files and export compiled models.
- Use timestamps: true instead of manually tracking dates.
- Validate at the schema level, not just in application logic.
- Always handle connection and disconnection events.
- Use .lean() for read-heavy, non-mutating queries.
graceful-shutdown.js
process.on('SIGINT', async () => {
await mongoose.connection.close();
console.log('Mongoose connection closed đ');
process.exit(0);
});25. Common Mistakes đĢ
| Mistake | Why It's a Problem |
|---|---|
| Forgetting runValidators: true on updates | Bypasses schema validation silently |
| Overusing populate() | Causes N+1-like performance issues |
| Not calling .lean() for read-only data | Wastes memory hydrating full documents |
| Defining models multiple times | Throws OverwriteModelError on hot reloads |
| Ignoring connection error events | Silent failures go unnoticed in production |
Caution
26. Frequently Asked Questions â
Question
Answer
Question
Answer
Question
Answer
27. Summary đ
Summary
- Official Docs: Mongoose Documentation
- API Reference: Mongoose API