- Installation
- Connecting to MongoDB
- Defining Schemas
- Creating Models
- CRUD Operations
- Querying
- Population
- Middleware
- Validation
- Indexing
- Virtuals
- Plugins
- Transactions
- Error Handling
- Best Practices
Install Mongoose in your project:
npm install mongoose
Install Mongoose in your project:
const mongoose = require('mongoose');
// Connect to local MongoDB
mongoose.connect('mongodb://localhost/mydatabase', {
useNewUrlParser: true,
useUnifiedTopology: true
});
// Connect to MongoDB Atlas
mongoose.connect('mongodb+srv://<username>:<password>@cluster0.mongodb.net/mydatabase', {
useNewUrlParser: true,
useUnifiedTopology: true
});
// Handle connection events
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', function() {
console.log('Connected to MongoDB');
});
Schemas define the structure of documents in a collection.
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
// Basic schema
const userSchema = new Schema({
name: String,
email: String,
age: Number
});
// Schema with more options
const productSchema = new Schema({
name: {
type: String,
required: true,
trim: true
},
price: {
type: Number,
min: 0
},
category: {
type: String,
enum: ['Electronics', 'Books', 'Clothing']
},
inStock: {
type: Boolean,
default: true
},
createdAt: {
type: Date,
default: Date.now
}
});
Models are fancy constructors compiled from Schema definitions.
const User = mongoose.model('User', userSchema);
const Product = mongoose.model('Product', productSchema);
// Create a single document
const newUser = new User({
name: 'John Doe',
email: '[email protected]',
age: 30
});
newUser.save((err, user) => {
if (err) return console.error(err);
console.log('User saved:', user);
});
// Create multiple documents
User.create([
{ name: 'Jane Doe', email: '[email protected]', age: 25 },
{ name: 'Bob Smith', email: '[email protected]', age: 35 }
], (err, users) => {
if (err) return console.error(err);
console.log('Users created:', users);
});
// Find all documents
User.find({}, (err, users) => {
if (err) return console.error(err);
console.log('All users:', users);
});
// Find documents with specific criteria
User.find({ age: { $gte: 18 } }, (err, users) => {
if (err) return console.error(err);
console.log('Adult users:', users);
});
// Find one document
User.findOne({ email: '[email protected]' }, (err, user) => {
if (err) return console.error(err);
console.log('Found user:', user);
});
// Find by ID
User.findById('5f7c3b3f9d3e2a1234567890', (err, user) => {
if (err) return console.error(err);
console.log('User by ID:', user);
});
// Update one document
User.updateOne({ name: 'John Doe' }, { age: 31 }, (err, result) => {
if (err) return console.error(err);
console.log('Update result:', result);
});
// Update multiple documents
User.updateMany({ age: { $lt: 18 } }, { isMinor: true }, (err, result) => {
if (err) return console.error(err);
console.log('Update result:', result);
});
// Find and update
User.findOneAndUpdate(
{ email: '[email protected]' },
{ $inc: { age: 1 } },
{ new: true },
(err, updatedUser) => {
if (err) return console.error(err);
console.log('Updated user:', updatedUser);
}
);
// Delete one document
User.deleteOne({ name: 'John Doe' }, (err) => {
if (err) return console.error(err);
console.log('User deleted');
});
// Delete multiple documents
User.deleteMany({ age: { $lt: 18 } }, (err) => {
if (err) return console.error(err);
console.log('Minor users deleted');
});
// Find and delete
User.findOneAndDelete({ email: '[email protected]' }, (err, deletedUser) => {
if (err) return console.error(err);
console.log('Deleted user:', deletedUser);
});
Mongoose provides a rich query API.
// Basic querying
User.find({ age: { $gte: 18 } })
.sort({ name: 1 })
.limit(10)
.select('name email')
.exec((err, users) => {
if (err) return console.error(err);
console.log('Adult users:', users);
});
// Chaining queries
User.find({ isActive: true })
.where('age').gte(18).lte(65)
.where('email').ne(null)
.limit(50)
.sort('-lastLogin')
.select('name email')
.exec((err, users) => {
if (err) return console.error(err);
console.log('Active adult users:', users);
});
// Using query builders
const query = User.find({ isActive: true });
query.where('age').gte(18).lte(65);
query.where('email').ne(null);
query.limit(50).sort('-lastLogin').select('name email');
query.exec((err, users) => {
if (err) return console.error(err);
console.log('Active adult users:', users);
});
// Using $or operator
User.find({
$or: [
{ age: { $lt: 18 } },
{ age: { $gt: 65 } }
]
}, (err, users) => {
if (err) return console.error(err);
console.log('Users not of working age:', users);
});
// Using regex
User.find({ name: /^John/ }, (err, users) => {
if (err) return console.error(err);
console.log('Users with names starting with John:', users);
});
Population is the process of automatically replacing specified paths in the document with document(s) from other collection(s).
// Define schemas with references
const authorSchema = new Schema({
name: String,
bio: String
});
const bookSchema = new Schema({
title: String,
author: { type: Schema.Types.ObjectId, ref: 'Author' }
});
const Author = mongoose.model('Author', authorSchema);
const Book = mongoose.model('Book', bookSchema);
// Create an author and a book
const author = new Author({ name: 'John Doe', bio: 'A prolific writer' });
author.save((err, savedAuthor) => {
if (err) return console.error(err);
const book = new Book({ title: 'My Great Novel', author: savedAuthor._id });
book.save((err, savedBook) => {
if (err) return console.error(err);
console.log('Book saved:', savedBook);
});
});
// Populate the author when querying for books
Book.findOne({ title: 'My Great Novel' })
.populate('author')
.exec((err, book) => {
if (err) return console.error(err);
console.log('Book with author details:', book);
});
// Populate specific fields
Book.findOne({ title: 'My Great Novel' })
.populate('author', 'name')
.exec((err, book) => {
if (err) return console.error(err);
console.log('Book with author name:', book);
});
// Nested population
const publisherSchema = new Schema({
name: String,
location: String
});
const bookSchema = new Schema({
title: String,
author: { type: Schema.Types.ObjectId, ref: 'Author' },
publisher: { type: Schema.Types.ObjectId, ref: 'Publisher' }
});
const Publisher = mongoose.model('Publisher', publisherSchema);
const Book = mongoose.model('Book', bookSchema);
Book.findOne({ title: 'My Great Novel' })
.populate({
path: 'author',
populate: { path: 'publisher' }
})
.exec((err, book) => {
if (err) return console.error(err);
console.log('Book with author and publisher details:', book);
});
Middleware (pre and post hooks) are functions which are passed control during execution of asynchronous functions.
// Pre-save middleware
userSchema.pre('save', function(next) {
// 'this' refers to the document being saved
if (this.isModified('password')) {
this.password = hashPassword(this.password);
}
next();
});
// Post-save middleware
userSchema.post('save', function(doc, next) {
console.log('%s has been saved', doc._id);
next();
});
// Pre-find middleware
userSchema.pre('find', function() {
// 'this' refers to the query
this.start = Date.now();
});
userSchema.post('find', function(result) {
console.log('Query took %d milliseconds', Date.now() - this.start);
});
// Error handling middleware
userSchema.post('save', function(error, doc, next) {
if (error.name === 'MongoError' && error.code === 11000) {
next(new Error('There was a duplicate key error'));
} else {
next(error);
}
});
Mongoose provides built-in and custom validators.
const userSchema = new Schema({
name: {
type: String,
required: true,
minlength: 2,
maxlength: 50
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
validate: {
validator: function(v) {
return /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(v);
},
message: props => `${props.value} is not a valid email address!`
}
},
age: {
type: Number,
min: [18, 'Must be at least 18 years old'],
max: [120, 'Age seems unrealistic']
},
interests: {
type: [String],
validate: {
validator: function(v) {
return v && v.length > 0;
},
message: 'A user must have at least one interest'
}
}
});
// Custom async validator
userSchema.path('email').validate(async function(value) {
const emailCount = await mongoose.models.User.countDocuments({ email: value });
return !emailCount;
}, 'Email already exists');
// Using validate()
const user = new User({
name: 'J',
email: 'invalid-email',
age: 15,
interests: []
});
user.validate((err) => {
if (err) console.log(err.message);
});
Indexes support the efficient execution of queries in MongoDB.
const userSchema = new Schema({
name: String,
email: { type: String, unique: true },
createdAt: Date
});
// Single field index
userSchema.index({ name: 1 });
// Compound index
userSchema.index({ name: 1, createdAt: -1 });
// Text index
userSchema.index({ name: 'text', email: 'text' });
// Geospatial index
const locationSchema = new Schema({
name: String,
location: {
type: { type: String, default: 'Point' },
coordinates: [Number]
}
});
locationSchema.index({ location: '2dsphere' });
// Creating indexes
mongoose.connect('mongodb://localhost/test', function(error) {
if (error) console.error(error);
else console.log('connected');
User.createIndexes(function(error) {
if (error) console.error(error);
else console.log('indexes created');
});
});
Virtuals are document properties that you can get and set but that do not get persisted to MongoDB.
const personSchema = new Schema({
firstName: String,
lastName: String
});
// Virtual for full name
personSchema.virtual('fullName')
.get(function() {
return this.firstName + ' ' + this.lastName;
})
.set(function(v) {
this.firstName = v.substr(0, v.indexOf(' '));
this.lastName = v.substr(v.indexOf(' ') + 1);
});
const Person = mongoose.model('Person', personSchema);
const person = new Person({
firstName: 'John',
lastName: 'Doe'
});
console.log(person.fullName); // 'John Doe'
person.fullName = 'Jane Smith';
console.log(person.firstName); // 'Jane'
console.log(person.lastName); // 'Smith'
Plugins are a tool for reusing logic in multiple schemas.
// Define a plugin
const lastModifiedPlugin = function(schema, options) {
schema.add({ lastMod: Date });
schema.pre('save', function(next) {
this.lastMod = new Date();
next();
});
if (options && options.index) {
schema.path('lastMod').index(options.index);
}
}
// Use the plugin
const userSchema = new Schema({ name: String, email: String });
userSchema.plugin(lastModifiedPlugin, { index: true });
// Alternatively, apply the plugin to all schemas
mongoose.plugin(lastModifiedPlugin);
Transactions let you execute multiple operations in isolation and potentially undo all the operations if one of them fails.
const session = await mongoose.startSession();
session.startTransaction();
try {
const opts = { session };
const A = await Account.findOne({ name: 'A' }, null, opts);
const B = await Account.findOne({ name: 'B' }, null, opts);
A.balance -= 100;
B.balance += 100;
await A.save(opts);
await B.save(opts);
await session.commitTransaction();
session.endSession();
} catch (error) {
await session.abortTransaction();
session.endSession();
throw error;
}
Proper error handling is crucial for robust applications. Here are some ways to handle errors in Mongoose:
// Using callbacks
User.findById(id, (err, user) => {
if (err) {
if (err.name === 'CastError') {
return console.error('Invalid ID');
}
return console.error(err);
}
console.log(user);
});
// Using promises
User.findById(id)
.then(user => {
console.log(user);
})
.catch(err => {
if (err.name === 'CastError') {
console.error('Invalid ID');
} else {
console.error(err);
}
});
// Using async/await
async function findUser(id) {
try {
const user = await User.findById(id);
console.log(user);
} catch (err) {
if (err.name === 'CastError') {
console.error('Invalid ID');
} else {
console.error(err);
}
}
}
// Global error handler for Mongoose
mongoose.connection.on('error', err => {
console.error('MongoDB connection error:', err);
});
// Custom error handling middleware (for Express.js)
app.use((err, req, res, next) => {
if (err instanceof mongoose.Error.ValidationError) {
return res.status(400).json({ error: err.message });
}
if (err.name === 'MongoError' && err.code === 11000) {
return res.status(409).json({ error: 'Duplicate key error' });
}
next(err);
});
Here are some best practices to follow when using Mongoose:
- Use Schemas: Always define schemas for your models. This ensures data consistency and allows for validation.
const userSchema = new Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
age: { type: Number, min: 18 }
});
- Validation: Use built-in and custom validators to ensure data integrity.
userSchema.path('email').validate(function(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}, 'Invalid email format');
- Middleware: Use middleware for repetitive tasks like data transformation or logging.
userSchema.pre('save', function(next) {
this.updatedAt = Date.now();
next();
});
- Indexing: Create indexes for frequently queried fields to improve performance.
userSchema.index({ email: 1 }, { unique: true });
- Lean Queries: Use .lean() for read-only operations to get plain JavaScript objects instead of Mongoose documents.
User.find().lean().exec((err, users) => {
console.log(users); // Plain JavaScript objects
});
- Projections: Use projections to select only the fields you need.
User.find({}, 'name email', (err, users) => {
console.log(users); // Only name and email fields
});
- Population: Use population to work with references efficiently.
Post.find().populate('author').exec((err, posts) => {
console.log(posts); // Posts with author details
});
- Error Handling: Always handle potential errors, especially in production environments.
User.findById(id)
.then(user => {
// Handle success
})
.catch(err => {
// Handle error
});
- Connections: Manage database connections properly.
mongoose.connect(uri, { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => console.log('Connected to MongoDB'))
.catch(err => console.error('Could not connect to MongoDB', err));
- Transactions: Use transactions for operations that require atomicity.
const session = await mongoose.startSession();
session.startTransaction();
try {
// Perform operations
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
} finally {
session.endSession();
}
Remember, these are general guidelines. The best practices for your specific application may vary depending on your requirements and use case.