Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js. It provides a schema-based solution to model your application data and includes built-in type casting validation, query building, business logic hooks and more.
How to setup
To get started with Mongoose, You’ill need to install both MongoDB and the Mongoose package:
npm install mongoose
How to connect
const mongoose = require('mongoose');
// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/mydatabase', {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('MongoDB connected successfully'))
.catch(err => console.error('MongoDB connection error:', err));
// Alternative connection with async/await
async function connectDB() {
try {
await mongoose.connect('mongodb://localhost:27017/mydatabase');
console.log('MongoDB connected successfully');
} catch (error) {
console.error('MongoDB connection error:', error);
process.exit(1);
}
}
How to create Schemas
A Schema defines the structure of your document within a collection.
const mongoose = require('mongoose');
const { Schema } = mongoose;
// Create a Schema
const userSchema = new Schema({
name: {
type: String,
required: true,
trim: true
},
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true
},
password: {
type: String,
required: true,
minlength: 6
},
age: {
type: Number,
min: 18,
default: 18
},
createdAt: {
type: Date,
default: Date.now
},
isActive: {
type: Boolean,
default: true
},
tags: [String],
address: {
street: String,
city: String,
state: String,
zipCode: String
}
});
Creating Models
Models are fancy constructors compiled form Schema definitions. They are responsible for creating and reading documents from the MongoDB database.
// Create a model from the schema
const User = mongoose.model(‘User’, userSchema);
// Export the model
module.exports = User;
Let do CRUD Operations
Create
// Method 1: Create and save
const user = new User({
name: 'John Doe',
email: 'john@example.com',
password: 'password123',
age: 30,
tags: ['developer', 'node.js'],
address: {
street: '123 Main St',
city: 'New York',
state: 'NY',
zipCode: '10001'
}
});
// Save the user
await user.save();
// Method 2: Create method
const user = await User.create({
name: 'Jane Smith',
email: 'jane@example.com',
password: 'secure123',
age: 25
});
Read
// Find all users
const users = await User.find();
// Find by ID
const user = await User.findById('60d21b4667d0d8992e610c85');
// Find one document that matches criteria
const user = await User.findOne({ email: 'john@example.com' });
// Find with specific fields
const users = await User.find({}, 'name email'); // Only return name and email
// Find with conditions
const activeUsers = await User.find({ isActive: true, age: { $gte: 21 } });
// Pagination
const page = 1;
const limit = 10;
const users = await User.find()
.skip((page - 1) * limit)
.limit(limit);
// Sorting
const users = await User.find().sort({ name: 1 }); // 1 for ascending, -1 for descending
Update
// Find and update by ID
const updatedUser = await User.findByIdAndUpdate(
'60d21b4667d0d8992e610c85',
{ name: 'Updated Name' },
{ new: true } // Return the updated document
);
// Update one document
const result = await User.updateOne(
{ email: 'john@example.com' },
{ $set: { isActive: false } }
);
// Update many documents
const result = await User.updateMany(
{ age: { $lt: 21 } },
{ $set: { isActive: false } }
);
Delete
// Find and delete by ID
const deletedUser = await User.findByIdAndDelete('60d21b4667d0d8992e610c85');
// Delete one document
const result = await User.deleteOne({ email: 'john@example.com' });
// Delete many documents
const result = await User.deleteMany({ isActive: false });
Validation
Mongoose provides built-in and custom validators to ensure your data meets specific requirements.
const userSchema = new Schema({
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
validate: {
validator: function(v) {
return /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(v);
},
message: props => `${props.value} is not a valid email!`
}
},
website: {
type: String,
validate: {
validator: function(v) {
return /^(http|https):\/\/[^ "]+$/.test(v);
},
message: props => `${props.value} is not a valid URL!`
}
},
phone: {
type: String,
validate: {
validator: function(v) {
return /\d{3}-\d{3}-\d{4}/.test(v);
},
message: props => `${props.value} is not a valid phone number!`
}
}
});
Middleware
Mongoose provides middleware (pre and post hooks) for controlling the execution flow.
// Hash password before saving
const bcrypt = require('bcrypt');
userSchema.pre('save', async function(next) {
const user = this;
// Only hash the password if it's modified or new
if (!user.isModified('password')) return next();
try {
// Generate salt
const salt = await bcrypt.genSalt(10);
// Hash password
user.password = await bcrypt.hash(user.password, salt);
next();
} catch (error) {
next(error);
}
});
// Add a method to compare passwords
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
// Pre find hook - Example: always exclude inactive users
userSchema.pre('find', function() {
this.where({ isActive: true });
});
// Post save hook
userSchema.post('save', function(doc, next) {
console.log(`User ${doc.name} has been saved`);
next();
});
Relationships
References (Normalization)
// User Schema with references to posts
const userSchema = new Schema({
name: String,
email: String,
posts: [{
type: Schema.Types.ObjectId,
ref: 'Post'
}]
});
// Post Schema
const postSchema = new Schema({
title: String,
content: String,
author: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
}
});
const User = mongoose.model('User', userSchema);
const Post = mongoose.model('Post', postSchema);
// Creating related documents
const user = new User({ name: 'John', email: 'john@example.com' });
await user.save();
const post = new Post({
title: 'My first post',
content: 'Hello world!',
author: user._id
});
await post.save();
// Update user's posts array
user.posts.push(post._id);
await user.save();
// Populate references
const userWithPosts = await User.findById(user._id).populate('posts');
const postWithAuthor = await Post.findById(post._id).populate('author');
Embedded Documents (Denormalization)
const commentSchema = new Schema({
text: String,
createdAt: {
type: Date,
default: Date.now
},
user: {
name: String,
email: String
}
});
const postSchema = new Schema({
title: String,
content: String,
comments: [commentSchema]
});
const Post = mongoose.model('Post', postSchema);
// Create a post with embedded comments
const post = new Post({
title: 'Embedded Documents Demo',
content: 'This post demonstrates embedded documents',
comments: [
{
text: 'Great post!',
user: { name: 'Alice', email: 'alice@example.com' }
},
{
text: 'Thanks for sharing',
user: { name: 'Bob', email: 'bob@example.com' }
}
]
});
await post.save();
// Add a new comment
post.comments.push({
text: 'I agree with Alice',
user: { name: 'Charlie', email: 'charlie@example.com' }
});
await post.save();
Advanced Queries
Query Operators
// Comparison operators
const users = await User.find({
age: { $gt: 18, $lt: 65 }, // Greater than 18 and less than 65
tags: { $in: ['developer', 'designer'] } // Tags include either 'developer' or 'designer'
});
// Logical operators
const users = await User.find({
$or: [
{ age: { $lt: 18 } },
{ age: { $gt: 65 } }
],
$and: [
{ isActive: true },
{ email: { $exists: true } }
]
});
// Element operators
const users = await User.find({
website: { $exists: true }, // Field exists
address: { $type: 'object' } // Field is an object
});
// Array operators
const users = await User.find({
tags: { $all: ['developer', 'node.js'] }, // Contains all specified elements
'address.zipCode': { $regex: /^100/ } // Regex match on nested field
});
Aggregation Pipeline
const result = await User.aggregate([
// Stage 1: Match users over 18
{ $match: { age: { $gt: 18 } } },
// Stage 2: Group by state and count
{ $group: {
_id: '$address.state',
count: { $sum: 1 },
avgAge: { $avg: '$age' }
}},
// Stage 3: Sort by count descending
{ $sort: { count: -1 } },
// Stage 4: Limit to top 5
{ $limit: 5 },
// Stage 5: Project to rename fields
{ $project: {
state: '$_id',
count: 1,
avgAge: 1,
_id: 0
}}
]);
Best Practices
1 Always handle errors: Use try/catch blocks or .catch() with promises.
2 Create indexes for frequently queried fields:
userSchema.index({ email: 1 }, { unique: true });
userSchema.index({ name: 'text' }); // Text index for searching
3. Use lean queries for better performance when you don’t need Mongoose documents:
const users = await User.find().lean();
4. Limit returned fields to only what you need:
const users = await User.find({}, 'name email -_id');
5. Use appropriate validation to ensure data integrity.
6. Consider schema design carefully:
- Embed data when it’s always accessed together with the parent
- Reference data when it needs to be accessed independently
- Consider the document size limit (16MB)
7. Use transactions for operations that need to be atomic:
const session = await mongoose.startSession();
session.startTransaction();
try {
// Operations that need to be atomic
const user = await User.create([{ name: 'John' }], { session });
await Post.create([{ title: 'Post', author: user[0]._id }], { session });
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
8. Implement pagination for large collections.
9. Close the connection when your app terminates:
process.on('SIGINT', async () => {
await mongoose.connection.close();
process.exit(0);
});