MongoDB with Mongoose
Introduction to MongoDB and Mongoose
MongoDB is a NoSQL document database that stores data in flexible, JSON-like documents. Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js that provides a schema-based solution to model application data with built-in type casting, validation, query building, and business logic hooks.
Why MongoDB with Node.js?
- Both use JavaScript/JSON for data
- Flexible schema for rapid development
- Horizontal scalability
- Rich query language and aggregation framework
- Strong community and ecosystem
Setting Up MongoDB and Mongoose
First, install MongoDB locally or use MongoDB Atlas (cloud service), then install Mongoose in your project:
# Install Mongoose
npm install mongoose
# For environment variables
npm install dotenv
Create a database configuration file:
// config/database.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
};
module.exports = connectDB;
Update your main server file to connect to MongoDB:
// server.js
require('dotenv').config();
const express = require('express');
const connectDB = require('./config/database');
const app = express();
// Connect to MongoDB
connectDB();
app.use(express.json());
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Connection String Format: In your .env file, use:
MONGODB_URI=mongodb://localhost:27017/myapp for local MongoDB, or
MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/myapp for MongoDB Atlas.
Creating Mongoose Schemas and Models
Schemas define the structure of documents within a collection. Models are constructors compiled from schemas that create and read documents.
// models/User.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name is required'],
trim: true,
minlength: [2, 'Name must be at least 2 characters'],
maxlength: [50, 'Name cannot exceed 50 characters']
},
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
trim: true,
match: [/^\S+@\S+\.\S+$/, 'Please enter a valid email']
},
password: {
type: String,
required: [true, 'Password is required'],
minlength: [6, 'Password must be at least 6 characters'],
select: false // Don't include in queries by default
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user'
},
age: {
type: Number,
min: [18, 'Must be at least 18 years old'],
max: [120, 'Age must be realistic']
},
isActive: {
type: Boolean,
default: true
},
avatar: String,
tags: [String],
createdAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true // Adds createdAt and updatedAt automatically
});
// Create and export model
const User = mongoose.model('User', userSchema);
module.exports = User;
Schema Options:
required: Field must have a valueunique: Creates a unique indexdefault: Default value if none providedenum: Value must be from specified listmin/max: Minimum/maximum valuesmatch: Must match regex patterntrim: Remove whitespacelowercase/uppercase: Convert case
CRUD Operations with Mongoose
Creating Documents
// Create a single document
const createUser = async (req, res) => {
try {
const user = await User.create({
name: 'John Doe',
email: 'john@example.com',
password: 'password123',
age: 25
});
res.status(201).json({
success: true,
data: user
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
};
// Alternative method
const createUserAlt = async (req, res) => {
try {
const user = new User(req.body);
await user.save();
res.status(201).json({
success: true,
data: user
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
};
// Insert multiple documents
const createMultipleUsers = async (req, res) => {
try {
const users = await User.insertMany([
{ name: 'User 1', email: 'user1@example.com', password: 'pass123' },
{ name: 'User 2', email: 'user2@example.com', password: 'pass123' }
]);
res.status(201).json({
success: true,
count: users.length,
data: users
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
};
Reading Documents
// Find all documents
const getAllUsers = async (req, res) => {
try {
const users = await User.find();
res.status(200).json({
success: true,
count: users.length,
data: users
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// Find with query conditions
const getActiveUsers = async (req, res) => {
try {
const users = await User.find({ isActive: true });
res.status(200).json({
success: true,
count: users.length,
data: users
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// Find one document by ID
const getUserById = async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({
success: false,
error: 'User not found'
});
}
res.status(200).json({
success: true,
data: user
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// Find one with conditions
const getUserByEmail = async (req, res) => {
try {
const user = await User.findOne({ email: req.params.email });
if (!user) {
return res.status(404).json({
success: false,
error: 'User not found'
});
}
res.status(200).json({
success: true,
data: user
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
Updating Documents
// Update by ID
const updateUser = async (req, res) => {
try {
const user = await User.findByIdAndUpdate(
req.params.id,
req.body,
{
new: true, // Return updated document
runValidators: true // Run schema validators
}
);
if (!user) {
return res.status(404).json({
success: false,
error: 'User not found'
});
}
res.status(200).json({
success: true,
data: user
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
};
// Update one with conditions
const updateUserEmail = async (req, res) => {
try {
const result = await User.updateOne(
{ _id: req.params.id },
{ email: req.body.email }
);
res.status(200).json({
success: true,
data: result
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
};
// Update many documents
const deactivateOldUsers = async (req, res) => {
try {
const sixMonthsAgo = new Date();
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
const result = await User.updateMany(
{ createdAt: { $lt: sixMonthsAgo }, isActive: true },
{ isActive: false }
);
res.status(200).json({
success: true,
modifiedCount: result.modifiedCount
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
};
Deleting Documents
// Delete by ID
const deleteUser = async (req, res) => {
try {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
return res.status(404).json({
success: false,
error: 'User not found'
});
}
res.status(200).json({
success: true,
data: {}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// Delete one with conditions
const deleteInactiveUser = async (req, res) => {
try {
const result = await User.deleteOne({
_id: req.params.id,
isActive: false
});
if (result.deletedCount === 0) {
return res.status(404).json({
success: false,
error: 'No inactive user found with that ID'
});
}
res.status(200).json({
success: true,
data: {}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// Delete many documents
const deleteInactiveUsers = async (req, res) => {
try {
const result = await User.deleteMany({ isActive: false });
res.status(200).json({
success: true,
deletedCount: result.deletedCount
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
Delete vs Remove: The remove() method is deprecated. Use deleteOne(), deleteMany(), or findByIdAndDelete() instead.
Advanced Queries
// Query operators
const advancedQuery = async (req, res) => {
try {
// Comparison operators
const adults = await User.find({ age: { $gte: 18 } });
const youngAdults = await User.find({ age: { $gte: 18, $lt: 30 } });
const specificAges = await User.find({ age: { $in: [20, 25, 30] } });
// Logical operators
const activeAdmins = await User.find({
$and: [
{ role: 'admin' },
{ isActive: true }
]
});
const adminOrModerator = await User.find({
$or: [
{ role: 'admin' },
{ role: 'moderator' }
]
});
// String operators
const usersStartingWithJ = await User.find({
name: { $regex: '^J', $options: 'i' }
});
// Exists operator
const usersWithAvatar = await User.find({ avatar: { $exists: true } });
// Array operators
const usersWithTag = await User.find({ tags: 'javascript' });
const usersWithAllTags = await User.find({
tags: { $all: ['javascript', 'nodejs'] }
});
res.status(200).json({
success: true,
data: {
adults: adults.length,
youngAdults: youngAdults.length,
activeAdmins: activeAdmins.length
}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// Sorting, limiting, and selecting fields
const getUsersFiltered = async (req, res) => {
try {
const users = await User
.find({ isActive: true })
.select('name email age -_id') // Include name, email, age; exclude _id
.sort({ age: -1 }) // Sort by age descending
.limit(10) // Limit to 10 results
.skip(0); // Skip first 0 (pagination)
res.status(200).json({
success: true,
count: users.length,
data: users
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// Pagination
const getUsersPaginated = async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const users = await User
.find()
.limit(limit)
.skip(skip)
.sort({ createdAt: -1 });
const total = await User.countDocuments();
res.status(200).json({
success: true,
count: users.length,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
},
data: users
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
Population (Referencing Documents)
Population allows you to reference documents in other collections and automatically replace the specified paths with documents from other collections.
// models/Post.js
const mongoose = require('mongoose');
const postSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
content: {
type: String,
required: true
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
comments: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Comment'
}],
likes: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}]
}, {
timestamps: true
});
const Post = mongoose.model('Post', postSchema);
module.exports = Post;
// Using population
const getPostWithAuthor = async (req, res) => {
try {
const post = await Post
.findById(req.params.id)
.populate('author', 'name email'); // Populate author, select only name and email
if (!post) {
return res.status(404).json({
success: false,
error: 'Post not found'
});
}
res.status(200).json({
success: true,
data: post
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// Multiple population
const getPostWithDetails = async (req, res) => {
try {
const post = await Post
.findById(req.params.id)
.populate('author', 'name email avatar')
.populate('comments')
.populate('likes', 'name');
res.status(200).json({
success: true,
data: post
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
// Nested population
const getPostWithCommentAuthors = async (req, res) => {
try {
const post = await Post
.findById(req.params.id)
.populate('author', 'name')
.populate({
path: 'comments',
populate: {
path: 'author',
select: 'name avatar'
}
});
res.status(200).json({
success: true,
data: post
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
};
Practice Exercise:
- Create a User model with name, email, password, and role fields
- Implement CRUD operations for users
- Add query filters to get users by role and age range
- Create a Post model with a reference to User as author
- Implement population to fetch posts with author details
- Add pagination to the get all users endpoint
- Create an endpoint to search users by name using regex
Best Practices:
- Use indexes for frequently queried fields
- Don't over-populate; select only needed fields
- Use lean() for read-only queries (faster)
- Handle validation errors properly
- Use transactions for operations affecting multiple collections
- Create separate files for each model
- Use schema methods for reusable logic