Node.js & Express

MongoDB with Mongoose

25 min Lesson 16 of 40

Introduction to MongoDB and Mongoose

MongoDB is a NoSQL document database that stores data in flexible, JSON-like documents. Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js that provides a schema-based solution to model application data with built-in type casting, validation, query building, and business logic hooks.

Why MongoDB with Node.js?

  • Both use JavaScript/JSON for data
  • Flexible schema for rapid development
  • Horizontal scalability
  • Rich query language and aggregation framework
  • Strong community and ecosystem

Setting Up MongoDB and Mongoose

First, install MongoDB locally or use MongoDB Atlas (cloud service), then install Mongoose in your project:

# Install Mongoose
npm install mongoose

# For environment variables
npm install dotenv

Create a database configuration file:

// config/database.js
const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    const conn = await mongoose.connect(process.env.MONGODB_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });

    console.log(`MongoDB Connected: ${conn.connection.host}`);
  } catch (error) {
    console.error(`Error: ${error.message}`);
    process.exit(1);
  }
};

module.exports = connectDB;

Update your main server file to connect to MongoDB:

// server.js
require('dotenv').config();
const express = require('express');
const connectDB = require('./config/database');

const app = express();

// Connect to MongoDB
connectDB();

app.use(express.json());

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Connection String Format: In your .env file, use:
MONGODB_URI=mongodb://localhost:27017/myapp for local MongoDB, or
MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/myapp for MongoDB Atlas.

Creating Mongoose Schemas and Models

Schemas define the structure of documents within a collection. Models are constructors compiled from schemas that create and read documents.

// models/User.js
const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: [true, 'Name is required'],
    trim: true,
    minlength: [2, 'Name must be at least 2 characters'],
    maxlength: [50, 'Name cannot exceed 50 characters']
  },
  email: {
    type: String,
    required: [true, 'Email is required'],
    unique: true,
    lowercase: true,
    trim: true,
    match: [/^\S+@\S+\.\S+$/, 'Please enter a valid email']
  },
  password: {
    type: String,
    required: [true, 'Password is required'],
    minlength: [6, 'Password must be at least 6 characters'],
    select: false // Don't include in queries by default
  },
  role: {
    type: String,
    enum: ['user', 'admin', 'moderator'],
    default: 'user'
  },
  age: {
    type: Number,
    min: [18, 'Must be at least 18 years old'],
    max: [120, 'Age must be realistic']
  },
  isActive: {
    type: Boolean,
    default: true
  },
  avatar: String,
  tags: [String],
  createdAt: {
    type: Date,
    default: Date.now
  }
}, {
  timestamps: true // Adds createdAt and updatedAt automatically
});

// Create and export model
const User = mongoose.model('User', userSchema);
module.exports = User;

Schema Options:

  • required: Field must have a value
  • unique: Creates a unique index
  • default: Default value if none provided
  • enum: Value must be from specified list
  • min/max: Minimum/maximum values
  • match: Must match regex pattern
  • trim: Remove whitespace
  • lowercase/uppercase: Convert case

CRUD Operations with Mongoose

Creating Documents

// Create a single document
const createUser = async (req, res) => {
  try {
    const user = await User.create({
      name: 'John Doe',
      email: 'john@example.com',
      password: 'password123',
      age: 25
    });

    res.status(201).json({
      success: true,
      data: user
    });
  } catch (error) {
    res.status(400).json({
      success: false,
      error: error.message
    });
  }
};

// Alternative method
const createUserAlt = async (req, res) => {
  try {
    const user = new User(req.body);
    await user.save();

    res.status(201).json({
      success: true,
      data: user
    });
  } catch (error) {
    res.status(400).json({
      success: false,
      error: error.message
    });
  }
};

// Insert multiple documents
const createMultipleUsers = async (req, res) => {
  try {
    const users = await User.insertMany([
      { name: 'User 1', email: 'user1@example.com', password: 'pass123' },
      { name: 'User 2', email: 'user2@example.com', password: 'pass123' }
    ]);

    res.status(201).json({
      success: true,
      count: users.length,
      data: users
    });
  } catch (error) {
    res.status(400).json({
      success: false,
      error: error.message
    });
  }
};

Reading Documents

// Find all documents
const getAllUsers = async (req, res) => {
  try {
    const users = await User.find();

    res.status(200).json({
      success: true,
      count: users.length,
      data: users
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
};

// Find with query conditions
const getActiveUsers = async (req, res) => {
  try {
    const users = await User.find({ isActive: true });

    res.status(200).json({
      success: true,
      count: users.length,
      data: users
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
};

// Find one document by ID
const getUserById = async (req, res) => {
  try {
    const user = await User.findById(req.params.id);

    if (!user) {
      return res.status(404).json({
        success: false,
        error: 'User not found'
      });
    }

    res.status(200).json({
      success: true,
      data: user
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
};

// Find one with conditions
const getUserByEmail = async (req, res) => {
  try {
    const user = await User.findOne({ email: req.params.email });

    if (!user) {
      return res.status(404).json({
        success: false,
        error: 'User not found'
      });
    }

    res.status(200).json({
      success: true,
      data: user
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
};

Updating Documents

// Update by ID
const updateUser = async (req, res) => {
  try {
    const user = await User.findByIdAndUpdate(
      req.params.id,
      req.body,
      {
        new: true,        // Return updated document
        runValidators: true  // Run schema validators
      }
    );

    if (!user) {
      return res.status(404).json({
        success: false,
        error: 'User not found'
      });
    }

    res.status(200).json({
      success: true,
      data: user
    });
  } catch (error) {
    res.status(400).json({
      success: false,
      error: error.message
    });
  }
};

// Update one with conditions
const updateUserEmail = async (req, res) => {
  try {
    const result = await User.updateOne(
      { _id: req.params.id },
      { email: req.body.email }
    );

    res.status(200).json({
      success: true,
      data: result
    });
  } catch (error) {
    res.status(400).json({
      success: false,
      error: error.message
    });
  }
};

// Update many documents
const deactivateOldUsers = async (req, res) => {
  try {
    const sixMonthsAgo = new Date();
    sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);

    const result = await User.updateMany(
      { createdAt: { $lt: sixMonthsAgo }, isActive: true },
      { isActive: false }
    );

    res.status(200).json({
      success: true,
      modifiedCount: result.modifiedCount
    });
  } catch (error) {
    res.status(400).json({
      success: false,
      error: error.message
    });
  }
};

Deleting Documents

// Delete by ID
const deleteUser = async (req, res) => {
  try {
    const user = await User.findByIdAndDelete(req.params.id);

    if (!user) {
      return res.status(404).json({
        success: false,
        error: 'User not found'
      });
    }

    res.status(200).json({
      success: true,
      data: {}
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
};

// Delete one with conditions
const deleteInactiveUser = async (req, res) => {
  try {
    const result = await User.deleteOne({
      _id: req.params.id,
      isActive: false
    });

    if (result.deletedCount === 0) {
      return res.status(404).json({
        success: false,
        error: 'No inactive user found with that ID'
      });
    }

    res.status(200).json({
      success: true,
      data: {}
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
};

// Delete many documents
const deleteInactiveUsers = async (req, res) => {
  try {
    const result = await User.deleteMany({ isActive: false });

    res.status(200).json({
      success: true,
      deletedCount: result.deletedCount
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
};

Delete vs Remove: The remove() method is deprecated. Use deleteOne(), deleteMany(), or findByIdAndDelete() instead.

Advanced Queries

// Query operators
const advancedQuery = async (req, res) => {
  try {
    // Comparison operators
    const adults = await User.find({ age: { $gte: 18 } });
    const youngAdults = await User.find({ age: { $gte: 18, $lt: 30 } });
    const specificAges = await User.find({ age: { $in: [20, 25, 30] } });

    // Logical operators
    const activeAdmins = await User.find({
      $and: [
        { role: 'admin' },
        { isActive: true }
      ]
    });

    const adminOrModerator = await User.find({
      $or: [
        { role: 'admin' },
        { role: 'moderator' }
      ]
    });

    // String operators
    const usersStartingWithJ = await User.find({
      name: { $regex: '^J', $options: 'i' }
    });

    // Exists operator
    const usersWithAvatar = await User.find({ avatar: { $exists: true } });

    // Array operators
    const usersWithTag = await User.find({ tags: 'javascript' });
    const usersWithAllTags = await User.find({
      tags: { $all: ['javascript', 'nodejs'] }
    });

    res.status(200).json({
      success: true,
      data: {
        adults: adults.length,
        youngAdults: youngAdults.length,
        activeAdmins: activeAdmins.length
      }
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
};

// Sorting, limiting, and selecting fields
const getUsersFiltered = async (req, res) => {
  try {
    const users = await User
      .find({ isActive: true })
      .select('name email age -_id') // Include name, email, age; exclude _id
      .sort({ age: -1 }) // Sort by age descending
      .limit(10) // Limit to 10 results
      .skip(0); // Skip first 0 (pagination)

    res.status(200).json({
      success: true,
      count: users.length,
      data: users
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
};

// Pagination
const getUsersPaginated = async (req, res) => {
  try {
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 10;
    const skip = (page - 1) * limit;

    const users = await User
      .find()
      .limit(limit)
      .skip(skip)
      .sort({ createdAt: -1 });

    const total = await User.countDocuments();

    res.status(200).json({
      success: true,
      count: users.length,
      pagination: {
        page,
        limit,
        total,
        pages: Math.ceil(total / limit)
      },
      data: users
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
};

Population (Referencing Documents)

Population allows you to reference documents in other collections and automatically replace the specified paths with documents from other collections.

// models/Post.js
const mongoose = require('mongoose');

const postSchema = new mongoose.Schema({
  title: {
    type: String,
    required: true
  },
  content: {
    type: String,
    required: true
  },
  author: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true
  },
  comments: [{
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Comment'
  }],
  likes: [{
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  }]
}, {
  timestamps: true
});

const Post = mongoose.model('Post', postSchema);
module.exports = Post;
// Using population
const getPostWithAuthor = async (req, res) => {
  try {
    const post = await Post
      .findById(req.params.id)
      .populate('author', 'name email'); // Populate author, select only name and email

    if (!post) {
      return res.status(404).json({
        success: false,
        error: 'Post not found'
      });
    }

    res.status(200).json({
      success: true,
      data: post
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
};

// Multiple population
const getPostWithDetails = async (req, res) => {
  try {
    const post = await Post
      .findById(req.params.id)
      .populate('author', 'name email avatar')
      .populate('comments')
      .populate('likes', 'name');

    res.status(200).json({
      success: true,
      data: post
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
};

// Nested population
const getPostWithCommentAuthors = async (req, res) => {
  try {
    const post = await Post
      .findById(req.params.id)
      .populate('author', 'name')
      .populate({
        path: 'comments',
        populate: {
          path: 'author',
          select: 'name avatar'
        }
      });

    res.status(200).json({
      success: true,
      data: post
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
};

Practice Exercise:

  1. Create a User model with name, email, password, and role fields
  2. Implement CRUD operations for users
  3. Add query filters to get users by role and age range
  4. Create a Post model with a reference to User as author
  5. Implement population to fetch posts with author details
  6. Add pagination to the get all users endpoint
  7. Create an endpoint to search users by name using regex

Best Practices:

  • Use indexes for frequently queried fields
  • Don't over-populate; select only needed fields
  • Use lean() for read-only queries (faster)
  • Handle validation errors properly
  • Use transactions for operations affecting multiple collections
  • Create separate files for each model
  • Use schema methods for reusable logic