MongoDB is the world's most popular NoSQL database — used by companies like Google, Amazon, Forbes, Toyota, and millions of developers worldwide. It stores data as flexible JSON-like documents instead of rigid tables. This tutorial takes you from zero to building real MongoDB applications — no prior experience required.
What you'll learn
| Topic | What you'll be able to do |
|---|---|
| Setup | Install MongoDB and connect via CLI and Compass |
| Documents & Collections | Create and manage MongoDB schemas |
| CRUD | insertOne/Many, find, updateOne/Many, deleteOne/Many |
| Query Operators | Filter, sort, project, limit, skip |
| Aggregation Pipeline | Group, transform, join data |
| Indexes | Speed up queries |
| Schema Design | Model relationships in NoSQL |
| Mongoose | Use MongoDB with Node.js |
| Projects | Build 3 real apps |
Part 1 — What Is MongoDB?
MongoDB is a document database. Instead of tables with rows and columns, it stores data as documents (JSON objects) inside collections.
SQL analogy:
Database → Database
Table → Collection
Row → Document
Column → Field
MongoDB vs SQL
| Feature | MongoDB | MySQL/PostgreSQL |
|---|---|---|
| Data format | JSON documents | Tables (rows/columns) |
| Schema | Flexible (no schema required) | Fixed schema |
| Relationships | Embedding or references | JOINs |
| Scaling | Horizontal (sharding) | Vertical (mostly) |
| Transactions | Multi-document (4.0+) | Full ACID |
| Query language | MongoDB Query Language | SQL |
| Best for | Flexible schemas, rapid dev | Structured data, complex JOINs |
MongoDB is the default choice for:
- Node.js and JavaScript applications
- Apps with flexible or evolving schemas
- JSON APIs and content management
- Real-time analytics and logging
Part 2 — Setup
Option 1: MongoDB Atlas (Cloud — easiest, free tier)
- Go to mongodb.com/cloud/atlas
- Create a free account
- Create a free M0 cluster
- Add your IP to the allowlist
- Get your connection string
Option 2: Local Install
Windows:
# Download MongoDB Community Server from mongodb.com/try/download/community
# Run the installer, keep defaults
# MongoDB runs as a Windows Service automatically
# Verify
mongosh --version
macOS (Homebrew):
brew tap mongodb/brew
brew install mongodb-community
brew services start mongodb-community
# Verify
mongosh --version
Ubuntu/Debian:
curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | sudo gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg --dearmor
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
sudo apt update
sudo apt install -y mongodb-org
sudo systemctl start mongod
sudo systemctl enable mongod
Docker (any OS):
docker run -d \
--name mongodb \
-p 27017:27017 \
-e MONGO_INITDB_ROOT_USERNAME=admin \
-e MONGO_INITDB_ROOT_PASSWORD=secret \
mongo:7.0
Connect with mongosh
# Local
mongosh
# With auth
mongosh "mongodb://admin:secret@localhost:27017"
# Atlas
mongosh "mongodb+srv://username:password@cluster0.abc.mongodb.net"
MongoDB Compass (GUI)
Download MongoDB Compass — the official GUI client. Connect with the same connection string. Great for exploring data visually.
Basic mongosh commands
// Show all databases
show dbs
// Use (or create) a database
use myapp
// Show collections in current DB
show collections
// Get help
db.help()
Part 3 — Documents and Collections
What is a Document?
A MongoDB document is a JSON object stored in BSON (Binary JSON) format. It can contain:
- Strings, numbers, booleans
- Arrays
- Nested objects
- Dates, ObjectIds, binary data
// A typical document
{
_id: ObjectId("64a7b3c2f1e2d3a4b5c6d7e8"), // auto-generated unique ID
name: "Alice Johnson",
email: "alice@example.com",
age: 28,
address: { // Embedded document
street: "123 Main St",
city: "New York",
zip: "10001"
},
hobbies: ["reading", "coding", "hiking"], // Array
createdAt: new Date(),
isActive: true
}
ObjectId
Every document automatically gets an _id field with a unique ObjectId unless you specify one:
ObjectId("64a7b3c2f1e2d3a4b5c6d7e8")
// 64a7b3c2 = Unix timestamp (4 bytes)
// f1e2d3 = Machine identifier (3 bytes)
// a4b5 = Process ID (2 bytes)
// c6d7e8 = Random increment (3 bytes)
You can use any unique value as _id:
{ _id: "user-alice", name: "Alice" } // string
{ _id: 42, name: "Bob" } // number
{ _id: ObjectId(), name: "Carol" } // auto ObjectId (default)
Part 4 — CRUD Operations
CREATE — Insert Documents
// Switch to database
use bookstore
// Insert one document
db.books.insertOne({
title: "The Great Gatsby",
author: "F. Scott Fitzgerald",
year: 1925,
genre: "Fiction",
price: 12.99,
inStock: true,
tags: ["classic", "american", "novel"]
})
// Returns: { acknowledged: true, insertedId: ObjectId("...") }
// Insert multiple documents
db.books.insertMany([
{
title: "1984",
author: "George Orwell",
year: 1949,
genre: "Dystopia",
price: 10.99,
inStock: true,
tags: ["classic", "political", "fiction"]
},
{
title: "Dune",
author: "Frank Herbert",
year: 1965,
genre: "Science Fiction",
price: 14.99,
inStock: false,
tags: ["sci-fi", "epic", "classic"]
},
{
title: "Atomic Habits",
author: "James Clear",
year: 2018,
genre: "Self-Help",
price: 16.99,
inStock: true,
tags: ["habits", "productivity", "nonfiction"]
}
])
// Returns: { acknowledged: true, insertedIds: { '0': ..., '1': ..., '2': ... } }
READ — Find Documents
// Find ALL documents in collection
db.books.find()
// Pretty print
db.books.find().pretty()
// Find ONE document (returns first match)
db.books.findOne({ genre: "Fiction" })
// Find with filter
db.books.find({ inStock: true })
// Find by exact field value
db.books.find({ author: "George Orwell" })
// Find with multiple conditions (AND)
db.books.find({ genre: "Fiction", inStock: true })
// Count results
db.books.countDocuments({ inStock: true })
// Returns: 3
Query Operators
// Comparison operators
db.books.find({ price: { $gt: 12 } }) // price > 12
db.books.find({ price: { $gte: 12.99 } }) // price >= 12.99
db.books.find({ price: { $lt: 15 } }) // price < 15
db.books.find({ price: { $lte: 14.99 } }) // price <= 14.99
db.books.find({ price: { $ne: 12.99 } }) // price != 12.99
db.books.find({ price: { $in: [10.99, 14.99] } }) // price IN list
db.books.find({ price: { $nin: [10.99] } }) // price NOT IN list
// Logical operators
db.books.find({ $and: [{ price: { $gt: 10 } }, { inStock: true }] })
db.books.find({ $or: [{ genre: "Fiction" }, { genre: "Dystopia" }] })
db.books.find({ $nor: [{ genre: "Self-Help" }] }) // NOT any of
db.books.find({ inStock: { $not: { $eq: false } } }) // NOT equal
// Element operators
db.books.find({ price: { $exists: true } }) // field exists
db.books.find({ price: { $type: "double" } }) // field type
// Array operators
db.books.find({ tags: "classic" }) // array contains value
db.books.find({ tags: { $all: ["classic", "fiction"] } }) // contains all
db.books.find({ tags: { $size: 3 } }) // array has exactly 3 elements
db.books.find({ "tags.0": "classic" }) // first array element
// Regular expressions
db.books.find({ title: { $regex: /habit/i } }) // case-insensitive
db.books.find({ author: { $regex: "^Frank" } }) // starts with
Projection — Select Fields
// Include specific fields (1 = include, 0 = exclude)
db.books.find({}, { title: 1, author: 1, _id: 0 })
// Returns: [{ title: "...", author: "..." }, ...]
// Exclude specific fields
db.books.find({}, { price: 0, tags: 0 })
// Note: You cannot mix include and exclude (except for _id)
Sort, Limit, Skip
// Sort ascending (1) or descending (-1)
db.books.find().sort({ price: 1 }) // cheapest first
db.books.find().sort({ year: -1 }) // newest first
db.books.find().sort({ author: 1, price: -1 }) // multi-field sort
// Limit results
db.books.find().limit(2)
// Skip documents (for pagination)
db.books.find().skip(10).limit(10) // page 2 (10 per page)
// Chain them
db.books.find({ inStock: true })
.sort({ price: 1 })
.skip(0)
.limit(5)
UPDATE — Modify Documents
// Update ONE document (first match)
db.books.updateOne(
{ title: "1984" }, // filter
{ $set: { price: 11.99 } } // update
)
// Update MANY documents
db.books.updateMany(
{ inStock: false },
{ $set: { inStock: true, restockedAt: new Date() } }
)
// Update operators
db.books.updateOne(
{ title: "Atomic Habits" },
{
$set: { price: 17.99 }, // set field value
$inc: { views: 1 }, // increment by 1
$push: { tags: "bestseller" }, // add to array
$addToSet: { tags: "popular" }, // add to array (no duplicates)
$pull: { tags: "nonfiction" }, // remove from array
$unset: { oldField: "" }, // remove field
$rename: { oldName: "newName" }, // rename field
$currentDate: { updatedAt: true } // set to current date
}
)
// Upsert — insert if not found
db.books.updateOne(
{ title: "New Book" },
{ $set: { author: "New Author", price: 9.99 } },
{ upsert: true }
)
// Replace entire document (except _id)
db.books.replaceOne(
{ title: "Dune" },
{ title: "Dune", author: "Frank Herbert", year: 1965, price: 15.99 }
)
// findOneAndUpdate — returns the document
const updated = db.books.findOneAndUpdate(
{ title: "1984" },
{ $set: { price: 12.99 } },
{ returnDocument: "after" } // return updated doc
)
DELETE — Remove Documents
// Delete ONE document (first match)
db.books.deleteOne({ title: "Old Book" })
// Delete MANY documents
db.books.deleteMany({ inStock: false })
// Delete all documents in collection (careful!)
db.books.deleteMany({})
// Drop the entire collection
db.books.drop()
// Drop the database
db.dropDatabase()
Part 5 — Aggregation Pipeline
The aggregation pipeline is MongoDB's most powerful feature. Documents flow through stages that transform the data.
Collection → [$match] → [$group] → [$sort] → [$limit] → result
Basic Aggregation
// Count books by genre
db.books.aggregate([
{
$group: {
_id: "$genre", // group by genre field
count: { $sum: 1 }, // count documents
avgPrice: { $avg: "$price" },
minPrice: { $min: "$price" },
maxPrice: { $max: "$price" }
}
},
{
$sort: { count: -1 } // sort by count descending
}
])
Common Aggregation Stages
// $match — filter (like find)
{ $match: { inStock: true, price: { $gte: 10 } } }
// $project — reshape documents
{ $project: { title: 1, author: 1, discounted: { $multiply: ["$price", 0.9] } } }
// $group — group and aggregate
{ $group: { _id: "$genre", total: { $sum: "$price" } } }
// $sort — sort results
{ $sort: { total: -1 } }
// $limit — limit results
{ $limit: 5 }
// $skip — skip documents
{ $skip: 10 }
// $unwind — flatten array field
{ $unwind: "$tags" }
// $lookup — JOIN with another collection
{
$lookup: {
from: "authors",
localField: "author",
foreignField: "name",
as: "authorDetails"
}
}
// $addFields — add computed fields
{ $addFields: { totalWithTax: { $multiply: ["$price", 1.2] } } }
// $count — count documents
{ $count: "totalBooks" }
Aggregation Example: Sales Dashboard
// Assume orders collection:
// { customerId, product, quantity, price, date, region }
db.orders.aggregate([
// Stage 1: filter last 30 days
{
$match: {
date: { $gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) }
}
},
// Stage 2: add revenue field
{
$addFields: {
revenue: { $multiply: ["$quantity", "$price"] }
}
},
// Stage 3: group by region
{
$group: {
_id: "$region",
totalRevenue: { $sum: "$revenue" },
totalOrders: { $sum: 1 },
avgOrderValue: { $avg: "$revenue" },
topProducts: { $push: "$product" }
}
},
// Stage 4: sort by revenue
{ $sort: { totalRevenue: -1 } },
// Stage 5: project clean output
{
$project: {
region: "$_id",
totalRevenue: { $round: ["$totalRevenue", 2] },
totalOrders: 1,
avgOrderValue: { $round: ["$avgOrderValue", 2] },
_id: 0
}
}
])
$unwind — Work with Arrays
// Flatten tags array — creates one doc per tag
db.books.aggregate([
{ $unwind: "$tags" },
{
$group: {
_id: "$tags",
bookCount: { $sum: 1 },
books: { $push: "$title" }
}
},
{ $sort: { bookCount: -1 } }
])
$lookup — Join Collections
// orders collection: { _id, customerId, total }
// customers collection: { _id, name, email }
db.orders.aggregate([
{
$lookup: {
from: "customers",
localField: "customerId",
foreignField: "_id",
as: "customer"
}
},
{ $unwind: "$customer" },
{
$project: {
orderTotal: "$total",
customerName: "$customer.name",
customerEmail: "$customer.email"
}
}
])
Part 6 — Indexes
Indexes speed up queries dramatically. Without an index, MongoDB scans every document (COLLSCAN). With an index, it jumps to the right data (IXSCAN).
// View existing indexes
db.books.getIndexes()
// Always has: [{ key: { _id: 1 }, name: "_id_" }]
// Create a single-field index
db.books.createIndex({ author: 1 }) // ascending
db.books.createIndex({ price: -1 }) // descending
// Create a compound index (order matters!)
db.books.createIndex({ genre: 1, price: -1 })
// Unique index
db.users.createIndex({ email: 1 }, { unique: true })
// Text index (for text search)
db.books.createIndex({ title: "text", description: "text" })
// TTL index (auto-delete after N seconds)
db.sessions.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 3600 } // delete after 1 hour
)
// Partial index (only index documents matching filter)
db.books.createIndex(
{ price: 1 },
{ partialFilterExpression: { inStock: true } }
)
// Explain query — see if index is used
db.books.find({ author: "George Orwell" }).explain("executionStats")
// Look for: "winningPlan.stage": "IXSCAN" (good) vs "COLLSCAN" (needs index)
// Drop an index
db.books.dropIndex({ author: 1 })
db.books.dropIndex("author_1") // by name
// Drop all indexes (except _id)
db.books.dropIndexes()
When to add indexes
| Situation | Add index? |
|---|---|
Field used in find() filter |
Yes |
Field used in sort() |
Yes |
| Unique field (email, username) | Yes (unique) |
Field in aggregation $match |
Yes |
| Field with low cardinality (boolean) | Usually no |
| Small collection (<1000 docs) | Rarely needed |
| Write-heavy field | Consider cost |
Part 7 — Schema Design
MongoDB is schema-less, but good schema design matters. Two main patterns: embedding and referencing.
Embedding (Denormalization)
Store related data inside the same document.
// Embedded address inside user
{
_id: ObjectId("..."),
name: "Alice",
email: "alice@example.com",
address: {
street: "123 Main St",
city: "New York",
country: "US"
},
orders: [
{ product: "Book", price: 12.99, date: ISODate("2025-01-15") },
{ product: "Pen", price: 2.99, date: ISODate("2025-01-20") }
]
}
Use embedding when:
- Data is always accessed together
- One-to-one or one-to-few relationships
- Child data doesn't grow unboundedly
- Data doesn't need to be queried independently
Referencing (Normalization)
Store a reference (ID) to a document in another collection.
// Author document
{
_id: ObjectId("author1"),
name: "George Orwell",
bio: "Eric Arthur Blair..."
}
// Book document — references author
{
_id: ObjectId("book1"),
title: "1984",
authorId: ObjectId("author1"), // reference
price: 10.99
}
// Query with $lookup to join
db.books.aggregate([
{ $lookup: { from: "authors", localField: "authorId", foreignField: "_id", as: "author" } }
])
Use referencing when:
- Many-to-many relationships
- Data is accessed independently
- Data is large or grows unboundedly
- Many documents reference the same data
Embedding vs Referencing
| Factor | Embedding | Referencing |
|---|---|---|
| Read performance | Faster (single query) | Slower (join needed) |
| Write performance | Slower (update all embeds) | Faster (update once) |
| Data duplication | Yes | No |
| Document size | Can grow large | Stays small |
| Relationships | One-to-one, one-to-few | One-to-many, many-to-many |
| Example | User+address | Post+comments |
Common Patterns
Bucket Pattern — group time-series data:
// Instead of 1 doc per sensor reading, bucket by hour
{
deviceId: "sensor-1",
hour: ISODate("2025-01-15T10:00:00Z"),
readings: [
{ minute: 0, temp: 22.1 },
{ minute: 1, temp: 22.3 },
// ... 60 readings per document
],
count: 60,
avgTemp: 22.2
}
Outlier Pattern — handle documents that exceed array limits:
// Flag when array is too large
{
_id: ObjectId("..."),
title: "Popular Post",
comments: [...], // first 100 comments
hasMore: true // flag for additional collection
}
Part 8 — MongoDB with Node.js (Mongoose)
Mongoose is the most popular ODM (Object Data Modeling) library for MongoDB and Node.js.
Setup
mkdir my-app && cd my-app
npm init -y
npm install mongoose dotenv
// .env
MONGODB_URI=mongodb://localhost:27017/myapp
# or Atlas:
# MONGODB_URI=mongodb+srv://username:password@cluster0.abc.mongodb.net/myapp
Connect
// db.js
const mongoose = require('mongoose');
require('dotenv').config();
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI);
console.log('MongoDB connected');
} catch (err) {
console.error('Connection error:', err.message);
process.exit(1);
}
};
module.exports = connectDB;
Define a Schema
// 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']
},
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, 'Invalid email format']
},
password: {
type: String,
required: true,
minlength: 6,
select: false // exclude from queries by default
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user'
},
age: {
type: Number,
min: [0, 'Age cannot be negative'],
max: [150, 'Invalid age']
},
address: {
street: String,
city: String,
country: { type: String, default: 'US' }
},
hobbies: [String],
isActive: { type: Boolean, default: true },
lastLogin: Date
}, {
timestamps: true // adds createdAt and updatedAt automatically
});
// Virtual field (not stored in DB)
userSchema.virtual('displayName').get(function() {
return `${this.name} (${this.email})`;
});
// Instance method
userSchema.methods.isAdmin = function() {
return this.role === 'admin';
};
// Static method
userSchema.statics.findByEmail = function(email) {
return this.findOne({ email: email.toLowerCase() });
};
// Middleware (hooks)
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
// Hash password here if needed
next();
});
const User = mongoose.model('User', userSchema);
module.exports = User;
CRUD with Mongoose
const connectDB = require('./db');
const User = require('./models/User');
connectDB();
// CREATE
const createUser = async () => {
const user = new User({
name: 'Alice Johnson',
email: 'alice@example.com',
password: 'hashedpassword123',
age: 28,
hobbies: ['reading', 'coding']
});
const saved = await user.save();
console.log('Created:', saved._id);
// Or use create()
const user2 = await User.create({
name: 'Bob Smith',
email: 'bob@example.com',
password: 'hashedpassword456'
});
};
// READ
const readUsers = async () => {
// Find all
const all = await User.find();
// Find with filter
const active = await User.find({ isActive: true });
// Find one
const alice = await User.findOne({ email: 'alice@example.com' });
// Find by ID
const user = await User.findById('64a7b3c2f1e2d3a4b5c6d7e8');
// Select fields
const names = await User.find().select('name email -_id');
// Sort, limit, skip
const page1 = await User.find()
.sort({ createdAt: -1 })
.limit(10)
.skip(0);
// Populate (resolve references)
const posts = await Post.find().populate('authorId', 'name email');
};
// UPDATE
const updateUser = async (id) => {
// findByIdAndUpdate
const updated = await User.findByIdAndUpdate(
id,
{ $set: { age: 29 }, $push: { hobbies: 'yoga' } },
{ new: true, runValidators: true } // return updated doc + validate
);
// updateMany
await User.updateMany({ isActive: false }, { $set: { isActive: true } });
// findOneAndUpdate
const user = await User.findOneAndUpdate(
{ email: 'alice@example.com' },
{ $inc: { loginCount: 1 }, $set: { lastLogin: new Date() } },
{ new: true }
);
};
// DELETE
const deleteUser = async (id) => {
await User.findByIdAndDelete(id);
await User.deleteMany({ isActive: false });
};
Relationships with Mongoose
// Post model — references User
const postSchema = new mongoose.Schema({
title: { type: String, required: true },
content: String,
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User', // reference to User model
required: true
},
tags: [String],
likes: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }]
}, { timestamps: true });
const Post = mongoose.model('Post', postSchema);
// Create post
const post = await Post.create({
title: 'My First Post',
content: 'Hello MongoDB!',
author: userId
});
// Populate author details
const posts = await Post.find()
.populate('author', 'name email') // replace authorId with user object
.populate('likes', 'name'); // populate likes array too
Part 9 — Project 1: Todo REST API (Node.js + MongoDB)
npm install express mongoose dotenv
// server.js
const express = require('express');
const mongoose = require('mongoose');
require('dotenv').config();
const app = express();
app.use(express.json());
// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI)
.then(() => console.log('MongoDB connected'))
.catch(err => console.error(err));
// Todo Schema
const todoSchema = new mongoose.Schema({
text: { type: String, required: true, trim: true },
completed: { type: Boolean, default: false },
priority: { type: String, enum: ['low', 'medium', 'high'], default: 'medium' }
}, { timestamps: true });
const Todo = mongoose.model('Todo', todoSchema);
// GET all todos
app.get('/api/todos', async (req, res) => {
try {
const { completed, priority, page = 1, limit = 10 } = req.query;
const filter = {};
if (completed !== undefined) filter.completed = completed === 'true';
if (priority) filter.priority = priority;
const todos = await Todo.find(filter)
.sort({ createdAt: -1 })
.limit(Number(limit))
.skip((Number(page) - 1) * Number(limit));
const total = await Todo.countDocuments(filter);
res.json({ todos, total, page: Number(page), pages: Math.ceil(total / limit) });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// GET single todo
app.get('/api/todos/:id', async (req, res) => {
try {
const todo = await Todo.findById(req.params.id);
if (!todo) return res.status(404).json({ error: 'Todo not found' });
res.json(todo);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// POST create todo
app.post('/api/todos', async (req, res) => {
try {
const todo = await Todo.create(req.body);
res.status(201).json(todo);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// PATCH update todo
app.patch('/api/todos/:id', async (req, res) => {
try {
const todo = await Todo.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!todo) return res.status(404).json({ error: 'Todo not found' });
res.json(todo);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// DELETE todo
app.delete('/api/todos/:id', async (req, res) => {
try {
const todo = await Todo.findByIdAndDelete(req.params.id);
if (!todo) return res.status(404).json({ error: 'Todo not found' });
res.json({ message: 'Todo deleted' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Stats endpoint using aggregation
app.get('/api/todos/stats/summary', async (req, res) => {
try {
const stats = await Todo.aggregate([
{
$group: {
_id: '$priority',
count: { $sum: 1 },
completed: { $sum: { $cond: ['$completed', 1, 0] } }
}
},
{ $sort: { _id: 1 } }
]);
const total = await Todo.countDocuments();
const done = await Todo.countDocuments({ completed: true });
res.json({ total, done, pending: total - done, byPriority: stats });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.listen(3000, () => console.log('Server on http://localhost:3000'));
Test with curl:
# Create todo
curl -X POST http://localhost:3000/api/todos \
-H "Content-Type: application/json" \
-d '{"text":"Learn MongoDB","priority":"high"}'
# Get all
curl http://localhost:3000/api/todos
# Filter
curl "http://localhost:3000/api/todos?priority=high&completed=false"
# Update
curl -X PATCH http://localhost:3000/api/todos/<id> \
-H "Content-Type: application/json" \
-d '{"completed":true}'
# Stats
curl http://localhost:3000/api/todos/stats/summary
Part 10 — Project 2: Blog with Users and Posts
// models/User.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true, trim: true },
email: { type: String, required: true, unique: true, lowercase: true },
bio: String,
avatar: String
}, { timestamps: true });
module.exports = mongoose.model('User', userSchema);
// models/Post.js
const postSchema = new mongoose.Schema({
title: { type: String, required: true, trim: true },
slug: { type: String, required: true, unique: true },
content: { type: String, required: true },
excerpt: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
tags: [String],
published: { type: Boolean, default: false },
publishedAt: Date,
viewCount: { type: Number, default: 0 }
}, { timestamps: true });
// Auto-generate excerpt
postSchema.pre('save', function(next) {
if (this.isModified('content') && !this.excerpt) {
this.excerpt = this.content.substring(0, 200) + '...';
}
if (this.published && !this.publishedAt) {
this.publishedAt = new Date();
}
next();
});
module.exports = mongoose.model('Post', postSchema);
// routes/posts.js
const express = require('express');
const router = express.Router();
const Post = require('../models/Post');
// GET published posts
router.get('/', async (req, res) => {
try {
const { tag, page = 1, limit = 10, search } = req.query;
const filter = { published: true };
if (tag) filter.tags = tag;
if (search) filter.$text = { $search: search }; // requires text index
const posts = await Post.find(filter)
.populate('author', 'username avatar')
.sort({ publishedAt: -1 })
.limit(Number(limit))
.skip((Number(page) - 1) * Number(limit))
.select('-content'); // exclude full content in list view
const total = await Post.countDocuments(filter);
res.json({ posts, total, pages: Math.ceil(total / limit) });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// GET single post by slug
router.get('/:slug', async (req, res) => {
try {
const post = await Post.findOneAndUpdate(
{ slug: req.params.slug, published: true },
{ $inc: { viewCount: 1 } },
{ new: true }
).populate('author', 'username bio avatar');
if (!post) return res.status(404).json({ error: 'Post not found' });
res.json(post);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// GET popular tags
router.get('/meta/tags', async (req, res) => {
try {
const tags = await Post.aggregate([
{ $match: { published: true } },
{ $unwind: '$tags' },
{ $group: { _id: '$tags', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 20 },
{ $project: { tag: '$_id', count: 1, _id: 0 } }
]);
res.json(tags);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;
Part 11 — Project 3: E-commerce Analytics
Demonstrate MongoDB aggregation power with an e-commerce dataset:
// Sample data structure
// orders: { customerId, items: [{product, qty, price}], total, status, createdAt, region }
// Monthly revenue report
const monthlyRevenue = await db.collection('orders').aggregate([
{ $match: { status: 'completed' } },
{
$group: {
_id: {
year: { $year: '$createdAt' },
month: { $month: '$createdAt' }
},
revenue: { $sum: '$total' },
orders: { $sum: 1 },
avgOrderValue: { $avg: '$total' }
}
},
{ $sort: { '_id.year': -1, '_id.month': -1 } },
{
$project: {
period: {
$concat: [
{ $toString: '$_id.year' }, '-',
{ $toString: '$_id.month' }
]
},
revenue: { $round: ['$revenue', 2] },
orders: 1,
avgOrderValue: { $round: ['$avgOrderValue', 2] },
_id: 0
}
}
]).toArray();
// Best-selling products
const topProducts = await db.collection('orders').aggregate([
{ $match: { status: 'completed' } },
{ $unwind: '$items' },
{
$group: {
_id: '$items.product',
totalSold: { $sum: '$items.qty' },
totalRevenue: { $sum: { $multiply: ['$items.qty', '$items.price'] } }
}
},
{ $sort: { totalRevenue: -1 } },
{ $limit: 10 },
{
$project: {
product: '$_id',
totalSold: 1,
totalRevenue: { $round: ['$totalRevenue', 2] },
_id: 0
}
}
]).toArray();
// Customer lifetime value
const customerLTV = await db.collection('orders').aggregate([
{ $match: { status: 'completed' } },
{
$group: {
_id: '$customerId',
totalSpent: { $sum: '$total' },
orderCount: { $sum: 1 },
firstOrder: { $min: '$createdAt' },
lastOrder: { $max: '$createdAt' }
}
},
{
$addFields: {
avgOrderValue: { $divide: ['$totalSpent', '$orderCount'] },
daysSinceFirst: {
$divide: [
{ $subtract: [new Date(), '$firstOrder'] },
1000 * 60 * 60 * 24
]
}
}
},
{ $sort: { totalSpent: -1 } },
{ $limit: 100 }
]).toArray();
Part 12 — MongoDB vs Other Databases
| Feature | MongoDB | MySQL | PostgreSQL | Redis | Cassandra |
|---|---|---|---|---|---|
| Type | Document | Relational | Relational | Key-Value | Wide-Column |
| Schema | Flexible | Fixed | Fixed | None | Fixed |
| ACID | Multi-doc (4.0+) | Full | Full | Limited | Tunable |
| Joins | $lookup | Full SQL JOINs | Full SQL JOINs | No | No |
| Horizontal scale | Native sharding | Limited | Limited | Yes | Native |
| Aggregation | Pipeline | GROUP BY | GROUP BY | Limited | No |
| Text search | Built-in | FTS | FTS + tsvector | No | No |
| Geospatial | Yes | Limited | PostGIS | GEO commands | No |
| Best for | Flexible schemas, JSON | Web apps, CMS | Complex queries | Caching | Write-heavy |
Quick Reference
// Connection
mongoose.connect('mongodb://localhost:27017/mydb')
// CRUD
Model.create(data)
Model.find(filter).sort().limit().skip()
Model.findOne(filter)
Model.findById(id)
Model.findByIdAndUpdate(id, update, { new: true })
Model.findByIdAndDelete(id)
Model.updateMany(filter, update)
Model.deleteMany(filter)
Model.countDocuments(filter)
// Query operators
{ $eq, $ne, $gt, $gte, $lt, $lte } // comparison
{ $in, $nin } // array membership
{ $and, $or, $nor, $not } // logical
{ $exists, $type } // element
{ $regex } // pattern
{ $text: { $search: "keyword" } } // text search
// Update operators
{ $set, $unset } // set/remove fields
{ $inc, $mul } // numeric
{ $push, $pull, $addToSet } // arrays
{ $currentDate } // date
{ $rename } // rename field
// Aggregation stages
$match, $group, $sort, $limit, $skip
$project, $addFields, $unwind
$lookup, $count, $bucket
Common Mistakes
| Mistake | Problem | Fix |
|---|---|---|
| No indexes on query fields | COLLSCAN — very slow at scale | Add indexes on filtered/sorted fields |
| Unbounded arrays | Document can exceed 16MB limit | Use referencing or bucket pattern |
| ObjectId as string | findById("abc") fails |
Always use ObjectId() or let Mongoose handle it |
Not using .lean() |
Mongoose returns full objects with methods | Add .lean() for read-only queries (2-10x faster) |
Ignoring runValidators |
findByIdAndUpdate skips schema validation |
Always pass { runValidators: true } |
| Over-embedding | Documents grow too large | Balance embedding vs referencing |
| No error handling on save | Duplicate key crashes app | Wrap in try/catch, check err.code === 11000 |
| Storing passwords in plaintext | Security disaster | Always hash with bcrypt before saving |
MongoDB vs Related Terms
| Term | What it is |
|---|---|
| MongoDB | Document database (NoSQL) |
| Mongoose | ODM library for Node.js + MongoDB |
| Atlas | MongoDB's cloud database service |
| mongosh | MongoDB interactive shell |
| BSON | Binary JSON — MongoDB's internal format |
| Collection | MongoDB equivalent of a SQL table |
| Document | MongoDB equivalent of a SQL row |
| Aggregation Pipeline | MongoDB's equivalent of SQL GROUP BY + JOINs |
| Replica Set | MongoDB's high availability mechanism |
| Sharding | MongoDB's horizontal scaling mechanism |
Learning Path
| Stage | Topics | Time |
|---|---|---|
| 1. Basics | Install, CRUD, query operators | Week 1 |
| 2. Aggregation | Pipeline, $group, $lookup, $unwind | Week 2 |
| 3. Indexes | createIndex, explain, compound indexes | Week 2 |
| 4. Schema design | Embedding vs referencing, patterns | Week 3 |
| 5. Mongoose | Schema, models, validation, populate | Week 3-4 |
| 6. Performance | Profiler, slow queries, index optimization | Month 2 |
| 7. Advanced | Transactions, change streams, Atlas Search | Month 2-3 |
| 8. DevOps | Replica sets, sharding, backup, monitoring | Month 3+ |
Free resources:
- MongoDB University — free official courses
- MongoDB Manual — complete reference
- Mongoose docs — ODM documentation
- MongoDB Atlas — free cloud cluster to practice
Frequently Asked Questions
Do I need to define a schema before inserting data? No — MongoDB is schema-less by default. But using Mongoose schemas in Node.js is recommended for validation and consistency.
When should I use MongoDB vs PostgreSQL? Use MongoDB for flexible schemas, JSON APIs, rapid prototyping, and document-centric data. Use PostgreSQL for complex relational data, strong ACID requirements, and complex JOINs. Many apps use both.
Does MongoDB support transactions?
Yes — multi-document ACID transactions since MongoDB 4.0. Use session.startTransaction(). For single-document operations, MongoDB is always atomic.
How do I handle the 16MB document size limit? Design your schema to avoid documents that grow unboundedly. Use referencing instead of embedding for large arrays. Use GridFS for files larger than 16MB.
Is MongoDB free? MongoDB Community Server is free and open-source. MongoDB Atlas offers a free M0 tier (512MB). Production workloads use Atlas paid tiers or self-hosted Community/Enterprise.
What's the difference between find() and findOne()?
find() returns a cursor (array of all matches). findOne() returns a single document (the first match) or null. Use findOne() when you expect one result, find() when you expect multiple.