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

Comparison
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

Terminal
# 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

Terminal
# Install Mongoose
npm install mongoose

# Install dotenv for environment variables
npm install dotenv

Connecting to MongoDB

.env
MONGODB_URI=mongodb://localhost:27017/myapp
# Or for MongoDB Atlas:
# MONGODB_URI=mongodb+srv://username:[email protected]/myapp
config/database.js
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;
server.js
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

models/User.js
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

models/Product.js
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)

JavaScript
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)

JavaScript
// 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

JavaScript
// 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

JavaScript
// 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)

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'
  }]
}, { timestamps: true });

module.exports = mongoose.model('Post', postSchema);

Population (Joining)

JavaScript
// 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

models/Order.js
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

routes/users.js
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:

Task
// 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:

Task
// 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:

Task
// 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:

Task
// 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

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.