Skip to main content

MongoDB & Mongoose

MongoDB is a NoSQL database that stores data in JSON-like documents. Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js. It manages relationships between data, provides schema validation, and is used to translate between objects in code and the representation of those objects in MongoDB.

Setup

  1. Install Mongoose:
    npm install mongoose
    
  2. Ensure you have a MongoDB instance running (local or Atlas).

Connecting to MongoDB

const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    await mongoose.connect(process.env.MONGO_URI);
    console.log('MongoDB Connected...');
  } catch (err) {
    console.error(err.message);
    process.exit(1);
  }
};

connectDB();

Defining a Schema

Everything in Mongoose starts with a Schema. Each schema maps to a MongoDB collection and defines the shape of the documents within that collection. models/User.js
const mongoose = require('mongoose');

const UserSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true
  },
  email: {
    type: String,
    required: true,
    unique: true
  },
  password: {
    type: String,
    required: true
  },
  date: {
    type: Date,
    default: Date.now
  }
});

module.exports = mongoose.model('User', UserSchema);

CRUD Operations with Mongoose

Create

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

const newUser = new User({
  name: 'John Doe',
  email: 'john@example.com',
  password: 'hashedpassword123'
});

await newUser.save();

Read

// Find all
const users = await User.find();

// Find one by criteria
const user = await User.findOne({ email: 'john@example.com' });

// Find by ID
const userById = await User.findById('60d5ec...');

Update

// Find and update
const updatedUser = await User.findByIdAndUpdate(
  id,
  { $set: { name: 'Jane Doe' } },
  { new: true } // Return the updated document
);

Delete

await User.findByIdAndDelete(id);

Relationships (Population)

Mongoose allows you to reference documents in other collections.
const PostSchema = new mongoose.Schema({
  user: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  },
  title: String
});

// ...

// Fetch posts and populate user data
const posts = await Post.find().populate('user', ['name', 'email']);

Summary

  • Mongoose simplifies MongoDB interactions
  • Schemas define the structure of your data
  • Models are compiled from schemas for database operations
  • Use populate() to handle relationships between collections

Schema Validation and Types

const UserSchema = new mongoose.Schema({
  // String with validation
  email: {
    type: String,
    required: [true, 'Email is required'],
    unique: true,
    lowercase: true,
    trim: true,
    match: [/^\S+@\S+\.\S+$/, 'Please provide a valid email']
  },
  
  // String with enum
  role: {
    type: String,
    enum: {
      values: ['user', 'admin', 'moderator'],
      message: '{VALUE} is not a valid role'
    },
    default: 'user'
  },
  
  // Number with min/max
  age: {
    type: Number,
    min: [18, 'Must be at least 18'],
    max: [120, 'Invalid age']
  },
  
  // Array of strings
  tags: [String],
  
  // Nested object
  address: {
    street: String,
    city: String,
    zipCode: {
      type: String,
      validate: {
        validator: (v) => /^\d{5}(-\d{4})?$/.test(v),
        message: 'Invalid zip code'
      }
    }
  },
  
  // Reference to another model
  createdBy: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  }
}, {
  timestamps: true, // Adds createdAt and updatedAt
  toJSON: { virtuals: true },
  toObject: { virtuals: true }
});

Virtual Properties

// Virtual property (not stored in database)
UserSchema.virtual('fullName').get(function() {
  return `${this.firstName} ${this.lastName}`;
});

// Virtual for related data
UserSchema.virtual('posts', {
  ref: 'Post',
  localField: '_id',
  foreignField: 'author'
});

Instance and Static Methods

// Instance method (available on document)
UserSchema.methods.comparePassword = async function(candidatePassword) {
  return bcrypt.compare(candidatePassword, this.password);
};

UserSchema.methods.generateAuthToken = function() {
  return jwt.sign({ id: this._id }, process.env.JWT_SECRET);
};

// Static method (available on model)
UserSchema.statics.findByEmail = function(email) {
  return this.findOne({ email });
};

UserSchema.statics.getActiveUsers = function() {
  return this.find({ isActive: true });
};

// Usage
const user = await User.findByEmail('john@example.com');
const isValid = await user.comparePassword('password123');
const token = user.generateAuthToken();

Middleware (Hooks)

// Pre-save middleware
UserSchema.pre('save', async function(next) {
  // Only hash if password is modified
  if (!this.isModified('password')) return next();
  
  this.password = await bcrypt.hash(this.password, 12);
  next();
});

// Post-save middleware
UserSchema.post('save', function(doc) {
  console.log('User saved:', doc._id);
});

// Pre-find middleware (runs on find, findOne, etc.)
UserSchema.pre(/^find/, function(next) {
  // Exclude inactive users by default
  this.find({ isActive: { $ne: false } });
  next();
});

// Pre-remove middleware
UserSchema.pre('remove', async function(next) {
  // Delete all posts by this user
  await Post.deleteMany({ author: this._id });
  next();
});

Advanced Queries

// Query builder pattern
const users = await User
  .find({ role: 'user' })
  .select('name email -_id')  // Include/exclude fields
  .sort('-createdAt')         // Sort descending
  .skip(10)                   // Skip first 10
  .limit(5)                   // Limit to 5
  .populate('posts')          // Populate references
  .lean();                    // Return plain JS objects

// Complex filters
const results = await Product.find({
  price: { $gte: 100, $lte: 500 },
  category: { $in: ['electronics', 'computers'] },
  $or: [
    { stock: { $gt: 0 } },
    { preorder: true }
  ]
});

// Text search (requires text index)
await Product.createIndex({ name: 'text', description: 'text' });
const searchResults = await Product.find(
  { $text: { $search: 'laptop gaming' } },
  { score: { $meta: 'textScore' } }
).sort({ score: { $meta: 'textScore' } });

Aggregation Pipeline

// Sales report by category
const salesReport = await Order.aggregate([
  // Match orders from last month
  {
    $match: {
      createdAt: { $gte: new Date('2024-01-01') }
    }
  },
  // Unwind order items
  { $unwind: '$items' },
  // Group by category
  {
    $group: {
      _id: '$items.category',
      totalRevenue: { $sum: { $multiply: ['$items.price', '$items.quantity'] } },
      totalOrders: { $sum: 1 },
      avgOrderValue: { $avg: { $multiply: ['$items.price', '$items.quantity'] } }
    }
  },
  // Sort by revenue
  { $sort: { totalRevenue: -1 } },
  // Format output
  {
    $project: {
      category: '$_id',
      totalRevenue: { $round: ['$totalRevenue', 2] },
      totalOrders: 1,
      avgOrderValue: { $round: ['$avgOrderValue', 2] },
      _id: 0
    }
  }
]);

Transactions

const session = await mongoose.startSession();

try {
  session.startTransaction();
  
  // Perform multiple operations
  const order = await Order.create([{ ...orderData }], { session });
  
  await Product.updateOne(
    { _id: productId },
    { $inc: { stock: -quantity } },
    { session }
  );
  
  await User.updateOne(
    { _id: userId },
    { $push: { orders: order[0]._id } },
    { session }
  );
  
  await session.commitTransaction();
} catch (error) {
  await session.abortTransaction();
  throw error;
} finally {
  session.endSession();
}

Indexes for Performance

// Single field index
UserSchema.index({ email: 1 });

// Compound index
OrderSchema.index({ userId: 1, createdAt: -1 });

// Text index for search
ProductSchema.index({ name: 'text', description: 'text' });

// Unique index
UserSchema.index({ email: 1 }, { unique: true });

// TTL index (auto-delete after time)
SessionSchema.index({ createdAt: 1 }, { expireAfterSeconds: 3600 });