Introduction
MongoDB is a popular NoSQL database that stores data in flexible, JSON-like documents. Combined with Mongoose, an elegant MongoDB object modeling tool for Node.js, you can easily define schemas, perform CRUD operations, and manage relationships. In this lesson, you'll learn how to integrate MongoDB into your Node.js applications and build robust data persistence layers.
Learning Objectives
By the end of this lesson, you will be able to:
- Understand NoSQL databases and MongoDB concepts
- Install and configure MongoDB
- Perform CRUD operations with Mongoose
- Design database schemas and models
- Implement data validation and relationships
- Connect your Node.js app to MongoDB
Prerequisites
- Completion of Lessons 01-07
- Understanding of backend development
- MongoDB installed locally or cloud access (MongoDB Atlas)
Estimated Time
4 hours (including practice exercises)
What is MongoDB?
MongoDB is a document-oriented NoSQL database that provides high performance, high availability, and easy scalability.
Key Features
- Document-Based - Stores data in JSON-like BSON format
- Schema-less - Flexible data structure
- Scalable - Horizontal scaling with sharding
- High Performance - Fast read/write operations
- Rich Query Language - Powerful querying capabilities
SQL vs NoSQL
SQL (Relational) NoSQL (MongoDB)
------------------ -------------------
Tables Collections
Rows Documents
Columns Fields
Joins Embedded Documents
Schema Required Schema Optional
Vertical Scaling Horizontal Scaling
Installation and Setup
Option 1: Local Installation
# macOS (using Homebrew)
brew tap mongodb/brew
brew install mongodb-community
brew services start mongodb-community
# Windows
# Download from https://www.mongodb.com/try/download/community
# Install and start MongoDB service
# Linux (Ubuntu)
sudo apt-get install mongodb
sudo systemctl start mongodb
Option 2: MongoDB Atlas (Cloud)
1. Visit MongoDB Atlas
2. Create a free account
3. Create a cluster
4. Get your connection string
Install Mongoose
# Install Mongoose
npm install mongoose
# Install dotenv for environment variables
npm install dotenv
Connecting to MongoDB
MONGODB_URI=mongodb://localhost:27017/myapp
# Or for MongoDB Atlas:
# MONGODB_URI=mongodb+srv://username:[email protected]/myapp
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
});
console.log('MongoDB connected successfully');
} catch (error) {
console.error('MongoDB connection error:', error);
process.exit(1);
}
};
module.exports = connectDB;
require('dotenv').config();
const express = require('express');
const connectDB = require('./config/database');
const app = express();
// Connect to database
connectDB();
// Middleware
app.use(express.json());
// Routes
app.get('/', (req, res) => {
res.json({ message: 'API is running' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Creating Schemas and Models
Schemas define the structure of documents in a collection:
Basic Schema
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name is required'],
trim: true,
minlength: 2,
maxlength: 50
},
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
trim: true,
match: [/^\S+@\S+\.\S+$/, 'Please enter a valid email']
},
age: {
type: Number,
min: 0,
max: 120
},
isActive: {
type: Boolean,
default: true
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user'
},
createdAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true // Adds createdAt and updatedAt automatically
});
const User = mongoose.model('User', userSchema);
module.exports = User;
Schema with Methods
const mongoose = require('mongoose');
const productSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
description: String,
price: {
type: Number,
required: true,
min: 0
},
stock: {
type: Number,
default: 0,
min: 0
},
category: {
type: String,
required: true
},
images: [String],
ratings: {
average: { type: Number, default: 0 },
count: { type: Number, default: 0 }
}
}, { timestamps: true });
// Instance method
productSchema.methods.isInStock = function() {
return this.stock > 0;
};
// Static method
productSchema.statics.findByCategory = function(category) {
return this.find({ category });
};
// Virtual property
productSchema.virtual('discountedPrice').get(function() {
return this.price * 0.9; // 10% discount
});
const Product = mongoose.model('Product', productSchema);
module.exports = Product;
CRUD Operations
Create (Insert)
const User = require('./models/User');
// Create single document
const createUser = async () => {
try {
const user = new User({
name: 'John Doe',
email: '[email protected]',
age: 30
});
await user.save();
console.log('User created:', user);
} catch (error) {
console.error('Error:', error.message);
}
};
// Alternative: create method
const createUserAlt = async () => {
try {
const user = await User.create({
name: 'Jane Smith',
email: '[email protected]',
age: 25
});
console.log('User created:', user);
} catch (error) {
console.error('Error:', error.message);
}
};
// Create multiple documents
const createMultipleUsers = async () => {
try {
const users = await User.insertMany([
{ name: 'Alice', email: '[email protected]', age: 28 },
{ name: 'Bob', email: '[email protected]', age: 32 }
]);
console.log('Users created:', users);
} catch (error) {
console.error('Error:', error.message);
}
};
Read (Query)
// Find all documents
const getAllUsers = async () => {
const users = await User.find();
return users;
};
// Find with conditions
const getActiveUsers = async () => {
const users = await User.find({ isActive: true });
return users;
};
// Find one document
const getUserByEmail = async (email) => {
const user = await User.findOne({ email });
return user;
};
// Find by ID
const getUserById = async (id) => {
const user = await User.findById(id);
return user;
};
// Query with operators
const getAdultUsers = async () => {
const users = await User.find({
age: { $gte: 18 } // Greater than or equal to 18
});
return users;
};
// Complex queries
const searchUsers = async (searchTerm) => {
const users = await User.find({
$or: [
{ name: { $regex: searchTerm, $options: 'i' } },
{ email: { $regex: searchTerm, $options: 'i' } }
]
});
return users;
};
// Select specific fields
const getUserNames = async () => {
const users = await User.find().select('name email -_id');
return users;
};
// Sorting and pagination
const getUsersPaginated = async (page = 1, limit = 10) => {
const skip = (page - 1) * limit;
const users = await User.find()
.sort({ createdAt: -1 }) // Sort by newest first
.skip(skip)
.limit(limit);
const total = await User.countDocuments();
return {
users,
page,
totalPages: Math.ceil(total / limit),
total
};
};
Update
// Update one document
const updateUser = async (id, updates) => {
try {
const user = await User.findByIdAndUpdate(
id,
updates,
{ new: true, runValidators: true }
);
return user;
} catch (error) {
console.error('Error:', error.message);
}
};
// Update multiple documents
const deactivateOldUsers = async () => {
const result = await User.updateMany(
{ age: { $gte: 65 } },
{ isActive: false }
);
console.log(`Updated ${result.modifiedCount} users`);
};
// Find and update
const incrementAge = async (id) => {
const user = await User.findByIdAndUpdate(
id,
{ $inc: { age: 1 } }, // Increment age by 1
{ new: true }
);
return user;
};
Delete
// Delete one document
const deleteUser = async (id) => {
try {
const user = await User.findByIdAndDelete(id);
return user;
} catch (error) {
console.error('Error:', error.message);
}
};
// Delete multiple documents
const deleteInactiveUsers = async () => {
const result = await User.deleteMany({ isActive: false });
console.log(`Deleted ${result.deletedCount} users`);
};
Relationships
One-to-Many (Referenced)
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'
}]
}, { timestamps: true });
module.exports = mongoose.model('Post', postSchema);
Population (Joining)
// Get post with author details
const getPostWithAuthor = async (postId) => {
const post = await Post.findById(postId)
.populate('author', 'name email')
.populate('comments');
return post;
};
// Nested population
const getPostWithDetails = async (postId) => {
const post = await Post.findById(postId)
.populate({
path: 'author',
select: 'name email'
})
.populate({
path: 'comments',
populate: {
path: 'author',
select: 'name'
}
});
return post;
};
Embedded Documents
const mongoose = require('mongoose');
const orderSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
items: [{
product: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Product'
},
quantity: {
type: Number,
required: true,
min: 1
},
price: {
type: Number,
required: true
}
}],
totalAmount: {
type: Number,
required: true
},
status: {
type: String,
enum: ['pending', 'processing', 'shipped', 'delivered'],
default: 'pending'
},
shippingAddress: {
street: String,
city: String,
state: String,
zipCode: String,
country: String
}
}, { timestamps: true });
module.exports = mongoose.model('Order', orderSchema);
Complete API Example
const express = require('express');
const router = express.Router();
const User = require('../models/User');
// GET all users
router.get('/', async (req, res) => {
try {
const { page = 1, limit = 10, search } = req.query;
const query = search
? { name: { $regex: search, $options: 'i' } }
: {};
const users = await User.find(query)
.select('-__v')
.limit(limit * 1)
.skip((page - 1) * limit)
.sort({ createdAt: -1 });
const count = await User.countDocuments(query);
res.json({
users,
totalPages: Math.ceil(count / limit),
currentPage: page,
total: count
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET single user
router.get('/:id', async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// CREATE user
router.post('/', async (req, res) => {
try {
const user = new User(req.body);
await user.save();
res.status(201).json(user);
} catch (error) {
if (error.code === 11000) {
return res.status(400).json({ error: 'Email already exists' });
}
res.status(400).json({ error: error.message });
}
});
// UPDATE user
router.put('/:id', async (req, res) => {
try {
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 (error) {
res.status(400).json({ error: error.message });
}
});
// DELETE user
router.delete('/:id', async (req, res) => {
try {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ message: 'User deleted successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
Key Takeaways
- MongoDB is a flexible NoSQL document database
- Mongoose provides schema validation and modeling
- Schemas define document structure and validation rules
- CRUD operations are performed with async/await
- Population allows joining referenced documents
- Indexes improve query performance
- Validation ensures data integrity
- Relationships can be referenced or embedded
Practice Exercises
Exercise 1: Blog System
Create a complete blog database schema:
// Create models for:
// 1. User (name, email, password, bio)
// 2. Post (title, content, author, tags, published)
// 3. Comment (content, author, post)
// Implement all CRUD operations
Exercise 2: E-commerce Products
Build a product catalog with categories:
// Create Product model with:
// - Name, description, price, stock
// - Category (referenced)
// - Reviews (embedded)
// - Images array
// Implement search and filtering
Exercise 3: User Authentication
Add password hashing to User model:
// Install bcrypt: npm install bcrypt
// Add pre-save hook to hash password
// Add method to compare passwords
// Implement registration and login
Exercise 4: Data Aggregation
Use aggregation pipeline for analytics:
// Create aggregation queries to:
// 1. Count users by role
// 2. Calculate average product price by category
// 3. Find top 10 most commented posts
// 4. Get monthly user registration stats
Additional Resources
What's Next?
In Lesson 09, you'll learn about authentication and security. You'll implement user registration and login, hash passwords securely with bcrypt, generate and verify JWT tokens, and protect your routes with authentication middleware.