Toolmingo
Guides24 min read

MongoDB Tutorial for Beginners (2025): Learn MongoDB Step by Step

Complete MongoDB tutorial for absolute beginners. Learn MongoDB installation, CRUD operations, aggregation pipeline, indexes, schema design, and more with real examples.

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)

  1. Go to mongodb.com/cloud/atlas
  2. Create a free account
  3. Create a free M0 cluster
  4. Add your IP to the allowlist
  5. 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:


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.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools