Mongoose ODM
While the native MongoDB driver works, Mongoose provides an elegant abstraction layer with schemas, validation, and powerful query methods that make database operations intuitive and safe.
Why Mongoose Over Native Driver
The native MongoDB driver gives you raw power but no guardrails. Mongoose adds:
- Schema Definition - Define exactly what fields your documents should have
- Type Casting - Automatically convert "25" to number 25
- Validation - Ensure required fields exist, emails are valid, etc.
- Middleware - Run code before/after save, delete, etc.
- Query Building - Chain methods like .find().sort().limit()
MongoDB driver is like having raw access to a warehouse - you can put anything anywhere. Mongoose is like having organized shelving with labeled bins - everything has its place, and you get alerts if something doesn't fit.
Installing and Connecting
First, install Mongoose in your project:
npm install mongoose
Now connect to MongoDB using Mongoose:
// Import mongoose library
const mongoose = require('mongoose');
// Connection string - replace with your MongoDB URI
const MONGODB_URI = 'mongodb://localhost:27017/myapp';
// Connect to MongoDB with recommended options
mongoose.connect(MONGODB_URI)
.then(() => {
// Connection successful
console.log('Connected to MongoDB via Mongoose');
})
.catch((err) => {
// Connection failed - log error and exit
console.error('MongoDB connection error:', err);
process.exit(1);
});
// Export the connection for use in other files
module.exports = mongoose;
Understanding Schemas
A Schema defines the structure of documents in a collection. Think of it as a blueprint for your data:
const mongoose = require('mongoose');
// Define the schema - what fields a user document can have
const userSchema = new mongoose.Schema({
// username: required string, must be unique, 3-30 chars
username: {
type: String,
required: [true, 'Username is required'],
unique: true,
minlength: [3, 'Username must be at least 3 characters'],
maxlength: [30, 'Username cannot exceed 30 characters'],
trim: true
},
// email: required string, must be unique, validated format
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, 'Please enter a valid email']
},
// age: optional number, must be between 13 and 120
age: {
type: Number,
min: [13, 'Must be at least 13 years old'],
max: [120, 'Age seems invalid']
},
// role: string with predefined allowed values (enum)
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user'
},
// isActive: boolean flag, defaults to true
isActive: {
type: Boolean,
default: true
},
// createdAt: auto-set to current date on creation
createdAt: {
type: Date,
default: Date.now
}
});
// Create model from schema - 'User' becomes 'users' collection
const User = mongoose.model('User', userSchema);
// Export model to use in routes/controllers
module.exports = User;
Schema Type Options
Common schema types and their options:
- String - trim, lowercase, uppercase, minlength, maxlength, match (regex)
- Number - min, max
- Date - min, max
- Boolean - no special options
- Array - [String], [Number], or [{nested schema}]
- ObjectId - for references to other documents
CRUD with Mongoose
Create Documents
const User = require('./models/User');
// Method 1: Create instance then save
const user1 = new User({
username: 'johndoe',
email: 'john@example.com',
age: 25
});
// save() returns a promise - await or use .then()
await user1.save();
console.log('User saved:', user1._id);
// Method 2: Create directly (shorter syntax)
const user2 = await User.create({
username: 'janedoe',
email: 'jane@example.com',
age: 28,
role: 'admin'
});
// Method 3: Create multiple at once
const users = await User.insertMany([
{ username: 'user1', email: 'u1@test.com' },
{ username: 'user2', email: 'u2@test.com' },
{ username: 'user3', email: 'u3@test.com' }
]);
Read Documents
// Find all users - returns array
const allUsers = await User.find();
// Find with conditions - all active admins
const admins = await User.find({ role: 'admin', isActive: true });
// Find one document - returns single doc or null
const john = await User.findOne({ username: 'johndoe' });
// Find by ID - most common for single doc
const user = await User.findById('507f1f77bcf86cd799439011');
// Select specific fields only (projection)
const names = await User.find().select('username email');
// Exclude fields with minus sign
const noPasswords = await User.find().select('-password -__v');
// Sorting: 1 = ascending, -1 = descending
const newest = await User.find().sort({ createdAt: -1 });
// Pagination with skip and limit
const page = 2;
const limit = 10;
const paginated = await User.find()
.skip((page - 1) * limit)
.limit(limit);
// Count documents matching criteria
const activeCount = await User.countDocuments({ isActive: true });
Update Documents
// findByIdAndUpdate - returns the document
// { new: true } returns updated doc, not original
const updated = await User.findByIdAndUpdate(
'507f1f77bcf86cd799439011',
{ age: 26, role: 'moderator' },
{ new: true, runValidators: true }
);
// findOneAndUpdate - find by any field
const promoted = await User.findOneAndUpdate(
{ username: 'johndoe' },
{ role: 'admin' },
{ new: true }
);
// updateMany - bulk update, returns count
const result = await User.updateMany(
{ isActive: false },
{ $set: { role: 'user' } }
);
console.log(`Updated ${result.modifiedCount} users`);
// Using update operators
await User.findByIdAndUpdate(id, {
$inc: { loginCount: 1 }, // increment by 1
$push: { tags: 'verified' }, // add to array
$set: { lastLogin: new Date() }
});
Delete Documents
// Delete by ID - returns deleted document
const deleted = await User.findByIdAndDelete('507f1f77bcf86cd799439011');
// Delete first matching document
const removed = await User.findOneAndDelete({ username: 'johndoe' });
// Delete many - returns count of deleted
const result = await User.deleteMany({ isActive: false });
console.log(`Deleted ${result.deletedCount} inactive users`);
Query Operators
Mongoose supports all MongoDB query operators:
// Comparison operators
const adults = await User.find({ age: { $gte: 18 } }); // >= 18
const young = await User.find({ age: { $lt: 30 } }); // < 30
const range = await User.find({ age: { $gte: 18, $lte: 65 } }); // 18-65
const notUser = await User.find({ role: { $ne: 'user' } }); // not equal
// Array operators
const specific = await User.find({
role: { $in: ['admin', 'moderator'] } // matches any in array
});
const excluded = await User.find({
role: { $nin: ['banned', 'suspended'] } // not in array
});
// Logical operators
const complex = await User.find({
$or: [
{ role: 'admin' },
{ age: { $gte: 30 } }
]
});
const both = await User.find({
$and: [
{ isActive: true },
{ age: { $gte: 18 } }
]
});
// Regex for text search
const search = await User.find({
username: { $regex: 'john', $options: 'i' } // case-insensitive
});
// Check field exists
const hasAge = await User.find({ age: { $exists: true } });
Complete Express Integration
const express = require('express');
const mongoose = require('mongoose');
// Initialize express app
const app = express();
// Parse JSON request bodies
app.use(express.json());
// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/myapp')
.then(() => console.log('MongoDB connected'))
.catch(err => console.error(err));
// Define User schema inline for simplicity
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
age: Number,
createdAt: { type: Date, default: Date.now }
});
const User = mongoose.model('User', userSchema);
// GET all users
app.get('/api/users', async (req, res) => {
try {
// Fetch all users, exclude __v field
const users = await User.find().select('-__v');
res.json(users);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// GET single user by ID
app.get('/api/users/:id', async (req, res) => {
try {
const user = await User.findById(req.params.id);
// Return 404 if user not found
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (err) {
// Handle invalid ObjectId format
if (err.name === 'CastError') {
return res.status(400).json({ error: 'Invalid user ID' });
}
res.status(500).json({ error: err.message });
}
});
// POST create new user
app.post('/api/users', async (req, res) => {
try {
// Create user from request body
const user = await User.create(req.body);
// Return 201 Created status
res.status(201).json(user);
} catch (err) {
// Handle validation errors
if (err.name === 'ValidationError') {
return res.status(400).json({ error: err.message });
}
// Handle duplicate key (unique constraint)
if (err.code === 11000) {
return res.status(400).json({ error: 'Username or email already exists' });
}
res.status(500).json({ error: err.message });
}
});
// PUT update user
app.put('/api/users/:id', async (req, res) => {
try {
// Update and return new version, run validators
const user = await User.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// DELETE user
app.delete('/api/users/:id', async (req, res) => {
try {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// Return success message
res.json({ message: 'User deleted successfully' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Start server
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Video Resources
Knowledge Check
1. What does the { new: true } option do in findByIdAndUpdate?
2. Which method would you use to find multiple users where age is between 18 and 30?
3. What happens when you try to save a document that violates a unique constraint?
Build a Product API with Mongoose
Create a complete REST API for products:
- Define a Product schema with: name (required), price (required, min 0), category (enum), inStock (boolean), createdAt
- Implement all CRUD endpoints
- Add a GET /api/products/search?q=term endpoint with regex search
- Add pagination with /api/products?page=1&limit=10
- Handle all validation and error cases properly