Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
MongoDB & Mongoose
MongoDB is a NoSQL database that stores data in JSON-like documents. Think of it like a giant filing cabinet where each drawer (collection) holds folders (documents) that can each have a different structure — unlike a SQL database where every row in a table must follow the exact same column layout.
Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js. It manages relationships between data, provides schema validation, and translates between objects in your code and the representation of those objects in MongoDB. If MongoDB is the filing cabinet, Mongoose is the organizational system that ensures every folder you put in has the right labels and contents before it gets filed away.
Setup
- Install Mongoose:
- Ensure you have a MongoDB instance running (local or Atlas).
Connecting to MongoDB
const mongoose = require('mongoose');
const connectDB = async () => {
try {
// mongoose.connect() returns a promise -- no need for the old
// useNewUrlParser or useUnifiedTopology options (removed in Mongoose 6+)
await mongoose.connect(process.env.MONGO_URI);
console.log('MongoDB Connected...');
} catch (err) {
console.error(err.message);
// Exit with failure code -- a database connection is non-negotiable
// for most apps, so crashing early is the right call here
process.exit(1);
}
};
connectDB();
Production tip: Never hardcode your connection string. Always use environment variables. For local development, use a .env file with dotenv. For production, consider connection strings with retryWrites=true and w=majority for replica set write safety.
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
CRUD stands for Create, Read, Update, Delete — the four fundamental operations you will perform against any database. Mongoose wraps each of these in intuitive methods that return promises, so they work naturally with async/await.
Create
const User = require('./models/User');
// Option 1: Instantiate then save (useful when you need to
// modify the document before persisting it)
const newUser = new User({
name: 'John Doe',
email: 'john@example.com',
password: 'hashedpassword123'
});
await newUser.save();
// Option 2: Create directly (shorthand -- instantiates and saves in one step)
const anotherUser = await User.create({
name: 'Jane Doe',
email: 'jane@example.com',
password: 'hashedpassword456'
});
Read
// Find all -- returns every document in the collection (careful on large datasets!)
const users = await User.find();
// Find one by criteria -- returns the first match or null
const user = await User.findOne({ email: 'john@example.com' });
// Find by ID -- a shorthand for findOne({ _id: '60d5ec...' })
const userById = await User.findById('60d5ec...');
// Pitfall: findById returns null (not an error) if the ID does not exist.
// Always check the result before using it.
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. This is conceptually similar to a JOIN in SQL — you store the ID of a related document, and Mongoose can “populate” it by fetching the full document at query time.
Think of it like a library card catalog: each book card has the author’s ID number. When you want the author’s full name, you look them up by that ID. Population does that lookup automatically.
const PostSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId, // Stores only the _id reference
ref: 'User' // Tells Mongoose which collection to look up during populate()
},
title: String
});
// ...
// Fetch posts and populate user data -- the second argument
// limits which fields are returned from the User document
const posts = await Post.find().populate('user', ['name', 'email']);
Performance pitfall: populate() triggers a separate query for each referenced collection. For deeply nested populations or large result sets, this can cause performance problems. Consider using aggregate() with $lookup for complex joins, or denormalize frequently-accessed data directly into the document.
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
Virtuals are computed properties that exist on the document in your code but are never persisted to the database. They are like a spreadsheet formula cell — the value is derived on the fly from other fields.
// Virtual property (not stored in database)
// Note: virtuals use a regular function (not an arrow function)
// because they need access to 'this' -- the document instance
UserSchema.virtual('fullName').get(function() {
return `${this.firstName} ${this.lastName}`;
});
// Virtual populate -- creates a reverse relationship without storing
// an array of post IDs on the user document
UserSchema.virtual('posts', {
ref: 'Post',
localField: '_id', // Match User._id...
foreignField: 'author' // ...against Post.author
});
// Then use: User.findById(id).populate('posts')
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)
Mongoose middleware are functions that run at specific stages of the document lifecycle — before or after save, validate, remove, and query operations. Think of them like airport security checkpoints: every document passes through them at defined stages, and you can inspect, modify, or reject documents at each checkpoint.
// Pre-save middleware -- runs BEFORE the document is written to the database.
// This is the most common hook and the standard place to hash passwords.
UserSchema.pre('save', async function(next) {
// Only hash if password is modified -- without this check,
// the password would be re-hashed on every save, corrupting it
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 -- the regex /^find/ matches find, findOne, findById, etc.
// This is a query middleware (not document middleware), so 'this' refers to the query.
UserSchema.pre(/^find/, function(next) {
// Exclude inactive users by default -- acts like a global "soft delete" filter
this.find({ isActive: { $ne: false } });
next();
});
// Pre-remove middleware -- useful for cascading deletes.
// Caution: this only fires on document.remove(), NOT on Model.deleteMany()
// or Model.findByIdAndDelete(). If you use those methods, this hook is silently skipped.
UserSchema.pre('remove', async function(next) {
// Delete all posts by this user
await Post.deleteMany({ author: this._id });
next();
});
Advanced Queries
Mongoose queries are chainable, so you can build complex queries step by step — like assembling a pipeline where each method refines the result set further.
// Query builder pattern -- each chained method returns the query,
// so you can compose queries incrementally
const users = await User
.find({ role: 'user' })
.select('name email -_id') // Include name & email, exclude _id
.sort('-createdAt') // Sort descending (the '-' prefix means DESC)
.skip(10) // Skip first 10 results (for pagination)
.limit(5) // Return at most 5 documents
.populate('posts') // Replace ObjectId refs with full documents
.lean(); // Return plain JS objects instead of Mongoose documents
// .lean() is a significant performance win when you only need to read data --
// it skips hydrating full Mongoose document instances and their change tracking
// 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
The aggregation pipeline is MongoDB’s equivalent of SQL GROUP BY, HAVING, and complex analytical queries. It works like an assembly line: documents enter one end, pass through a series of processing stages, and the transformed results come out the other end. Each stage receives the output of the previous stage.
// Sales report by category
const salesReport = await Order.aggregate([
// Stage 1: Match orders from last month -- always filter early
// to reduce the number of documents flowing through later stages
{
$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
Transactions let you group multiple database operations into a single atomic unit — either all operations succeed, or none of them do. This is critical for operations like placing an order, where you need to create the order AND decrement inventory AND charge the user. If any step fails, you want everything rolled back to a consistent state.
Important: MongoDB transactions require a replica set (even for local development). If you are running a standalone mongod, transactions will fail. Use mongosh to initiate a replica set, or use MongoDB Atlas which provides one by default.
const session = await mongoose.startSession();
try {
session.startTransaction();
// Every operation in the transaction must include { session }
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 are like the index at the back of a textbook — instead of scanning every page (document) to find what you need, the database jumps directly to the right location. Without indexes, MongoDB performs a “collection scan” (reads every document), which gets painfully slow as your data grows.
// Single field index -- speeds up queries that filter or sort by email
// 1 = ascending order, -1 = descending
UserSchema.index({ email: 1 });
// Compound index -- covers queries that filter by userId AND sort by createdAt.
// The order of fields matters: put fields you filter with equality first,
// then range/sort fields second.
OrderSchema.index({ userId: 1, createdAt: -1 });
// Text index for full-text search -- only one text index per collection
ProductSchema.index({ name: 'text', description: 'text' });
// Unique index -- also enforces uniqueness at the database level
UserSchema.index({ email: 1 }, { unique: true });
// TTL index (auto-delete after time) -- MongoDB automatically removes
// documents once createdAt + expireAfterSeconds has passed.
// Perfect for session tokens, temporary data, or log entries.
SessionSchema.index({ createdAt: 1 }, { expireAfterSeconds: 3600 });
Index pitfall: Every index speeds up reads but slows down writes, because MongoDB must update all relevant indexes on every insert, update, or delete. Do not blindly add indexes on every field. Use explain() on your queries to see whether an index is actually being used, and remove unused indexes with db.collection.dropIndex().