--- name: mongodb description: Work with MongoDB databases using best practices. Use when designing schemas, writing queries, building aggregation pipelines, or optimizing performance. Triggers on MongoDB, Mongoose, NoSQL, aggregation pipeline, document database, MongoDB Atlas. --- # MongoDB & Mongoose Build and query MongoDB databases with best practices. ## Quick Start ```bash npm install mongodb mongoose ``` ### Native Driver ```typescript import { MongoClient, ObjectId } from 'mongodb'; const client = new MongoClient(process.env.MONGODB_URI!); const db = client.db('myapp'); const users = db.collection('users'); // Connect await client.connect(); // CRUD Operations await users.insertOne({ name: 'Alice', email: 'alice@example.com' }); const user = await users.findOne({ email: 'alice@example.com' }); await users.updateOne({ _id: user._id }, { $set: { name: 'Alice Smith' } }); await users.deleteOne({ _id: user._id }); ``` ### Mongoose Setup ```typescript import mongoose from 'mongoose'; await mongoose.connect(process.env.MONGODB_URI!, { maxPoolSize: 10, serverSelectionTimeoutMS: 5000, socketTimeoutMS: 45000, }); // Connection events mongoose.connection.on('connected', () => console.log('MongoDB connected')); mongoose.connection.on('error', (err) => console.error('MongoDB error:', err)); mongoose.connection.on('disconnected', () => console.log('MongoDB disconnected')); // Graceful shutdown process.on('SIGINT', async () => { await mongoose.connection.close(); process.exit(0); }); ``` ## Schema Design ### Basic Schema ```typescript import mongoose, { Schema, Document, Model } from 'mongoose'; interface IUser extends Document { email: string; name: string; password: string; role: 'user' | 'admin'; profile: { avatar?: string; bio?: string; }; createdAt: Date; updatedAt: Date; } const userSchema = new Schema({ email: { type: String, required: [true, 'Email is required'], unique: true, lowercase: true, trim: true, match: [/^\S+@\S+\.\S+$/, 'Invalid email format'], }, name: { type: String, required: true, trim: true, minlength: 2, maxlength: 100, }, password: { type: String, required: true, select: false, // Never return password by default }, role: { type: String, enum: ['user', 'admin'], default: 'user', }, profile: { avatar: String, bio: { type: String, maxlength: 500 }, }, }, { timestamps: true, // Adds createdAt, updatedAt toJSON: { transform(doc, ret) { delete ret.password; delete ret.__v; return ret; }, }, }); // Indexes userSchema.index({ email: 1 }); userSchema.index({ createdAt: -1 }); userSchema.index({ name: 'text', 'profile.bio': 'text' }); // Text search const User: Model = mongoose.model('User', userSchema); ``` ### Embedded Documents vs References ```typescript // ✅ Embed when: Data is read together, doesn't grow unbounded const orderSchema = new Schema({ customer: { name: String, email: String, address: { street: String, city: String, country: String, }, }, items: [{ product: String, quantity: Number, price: Number, }], total: Number, }); // ✅ Reference when: Data is large, shared, or changes independently const postSchema = new Schema({ title: String, content: String, author: { type: Schema.Types.ObjectId, ref: 'User', required: true, }, comments: [{ type: Schema.Types.ObjectId, ref: 'Comment', }], }); // Populate references const post = await Post.findById(id) .populate('author', 'name email') // Select specific fields .populate({ path: 'comments', populate: { path: 'author', select: 'name' }, // Nested populate }); ``` ### Virtuals ```typescript const userSchema = new Schema({ firstName: String, lastName: String, }); // Virtual field (not stored in DB) userSchema.virtual('fullName').get(function() { return `${this.firstName} ${this.lastName}`; }); // Virtual populate (for reverse references) userSchema.virtual('posts', { ref: 'Post', localField: '_id', foreignField: 'author', }); // Enable virtuals in JSON userSchema.set('toJSON', { virtuals: true }); userSchema.set('toObject', { virtuals: true }); ``` ## Query Operations ### Find Operations ```typescript // Find with filters const users = await User.find({ role: 'user', createdAt: { $gte: new Date('2024-01-01') }, }); // Query builder const results = await User.find() .where('role').equals('user') .where('createdAt').gte(new Date('2024-01-01')) .select('name email') .sort({ createdAt: -1 }) .limit(10) .skip(20) .lean(); // Return plain objects (faster) // Find one const user = await User.findOne({ email: 'alice@example.com' }); const userById = await User.findById(id); // Exists check const exists = await User.exists({ email: 'alice@example.com' }); // Count const count = await User.countDocuments({ role: 'admin' }); ``` ### Query Operators ```typescript // Comparison await User.find({ age: { $eq: 25 } }); // Equal await User.find({ age: { $ne: 25 } }); // Not equal await User.find({ age: { $gt: 25 } }); // Greater than await User.find({ age: { $gte: 25 } }); // Greater or equal await User.find({ age: { $lt: 25 } }); // Less than await User.find({ age: { $lte: 25 } }); // Less or equal await User.find({ age: { $in: [20, 25, 30] } }); // In array await User.find({ age: { $nin: [20, 25] } }); // Not in array // Logical await User.find({ $and: [{ age: { $gte: 18 } }, { role: 'user' }], }); await User.find({ $or: [{ role: 'admin' }, { isVerified: true }], }); await User.find({ age: { $not: { $lt: 18 } } }); // Element await User.find({ avatar: { $exists: true } }); await User.find({ score: { $type: 'number' } }); // Array await User.find({ tags: 'nodejs' }); // Array contains value await User.find({ tags: { $all: ['nodejs', 'mongodb'] } }); // Contains all await User.find({ tags: { $size: 3 } }); // Array length await User.find({ 'items.0.price': { $gt: 100 } }); // Array index // Text search await User.find({ $text: { $search: 'mongodb developer' } }); // Regex await User.find({ name: { $regex: /^john/i } }); ``` ### Update Operations ```typescript // Update one await User.updateOne( { _id: userId }, { $set: { name: 'New Name' } } ); // Update many await User.updateMany( { role: 'user' }, { $set: { isVerified: true } } ); // Find and update (returns document) const updated = await User.findByIdAndUpdate( userId, { $set: { name: 'New Name' } }, { new: true, runValidators: true } // Return updated doc, run validators ); // Update operators await User.updateOne({ _id: userId }, { $set: { name: 'New Name' }, // Set field $unset: { tempField: '' }, // Remove field $inc: { loginCount: 1 }, // Increment $mul: { score: 1.5 }, // Multiply $min: { lowScore: 50 }, // Set if less than $max: { highScore: 100 }, // Set if greater than $push: { tags: 'new-tag' }, // Add to array $pull: { tags: 'old-tag' }, // Remove from array $addToSet: { tags: 'unique-tag' }, // Add if not exists }); // Upsert (insert if not exists) await User.updateOne( { email: 'new@example.com' }, { $set: { name: 'New User' } }, { upsert: true } ); ``` ## Aggregation Pipeline ### Basic Aggregation ```typescript const results = await Order.aggregate([ // Stage 1: Match { $match: { status: 'completed' } }, // Stage 2: Group { $group: { _id: '$customerId', totalOrders: { $sum: 1 }, totalSpent: { $sum: '$total' }, avgOrder: { $avg: '$total' }, }}, // Stage 3: Sort { $sort: { totalSpent: -1 } }, // Stage 4: Limit { $limit: 10 }, ]); ``` ### Pipeline Stages ```typescript const pipeline = [ // $match - Filter documents { $match: { createdAt: { $gte: new Date('2024-01-01') } } }, // $project - Shape output { $project: { name: 1, email: 1, yearJoined: { $year: '$createdAt' }, fullName: { $concat: ['$firstName', ' ', '$lastName'] }, }}, // $lookup - Join collections { $lookup: { from: 'orders', localField: '_id', foreignField: 'userId', as: 'orders', }}, // $unwind - Flatten arrays { $unwind: { path: '$orders', preserveNullAndEmptyArrays: true } }, // $group - Aggregate { $group: { _id: '$_id', name: { $first: '$name' }, orderCount: { $sum: 1 }, orders: { $push: '$orders' }, }}, // $addFields - Add computed fields { $addFields: { hasOrders: { $gt: ['$orderCount', 0] }, }}, // $facet - Multiple pipelines { $facet: { topCustomers: [{ $sort: { orderCount: -1 } }, { $limit: 5 }], stats: [{ $group: { _id: null, avgOrders: { $avg: '$orderCount' } } }], }}, ]; ``` ### Analytics Examples ```typescript // Sales by month const salesByMonth = await Order.aggregate([ { $match: { status: 'completed' } }, { $group: { _id: { year: { $year: '$createdAt' }, month: { $month: '$createdAt' }, }, totalSales: { $sum: '$total' }, orderCount: { $sum: 1 }, }}, { $sort: { '_id.year': -1, '_id.month': -1 } }, ]); // Top products const topProducts = await Order.aggregate([ { $unwind: '$items' }, { $group: { _id: '$items.productId', totalQuantity: { $sum: '$items.quantity' }, totalRevenue: { $sum: { $multiply: ['$items.price', '$items.quantity'] } }, }}, { $lookup: { from: 'products', localField: '_id', foreignField: '_id', as: 'product', }}, { $unwind: '$product' }, { $project: { name: '$product.name', totalQuantity: 1, totalRevenue: 1, }}, { $sort: { totalRevenue: -1 } }, { $limit: 10 }, ]); ``` ## Middleware (Hooks) ```typescript // Pre-save middleware userSchema.pre('save', async function(next) { if (this.isModified('password')) { 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 userSchema.pre(/^find/, function(next) { // Exclude deleted users by default this.find({ isDeleted: { $ne: true } }); next(); }); // Pre-aggregate middleware userSchema.pre('aggregate', function(next) { // Add match stage to all aggregations this.pipeline().unshift({ $match: { isDeleted: { $ne: true } } }); next(); }); ``` ## Transactions ```typescript const session = await mongoose.startSession(); try { session.startTransaction(); // All operations in the transaction const user = await User.create([{ name: 'Alice' }], { session }); await Account.create([{ userId: user[0]._id, balance: 0 }], { session }); await Order.updateOne({ _id: orderId }, { $set: { status: 'paid' } }, { session }); await session.commitTransaction(); } catch (error) { await session.abortTransaction(); throw error; } finally { session.endSession(); } // With callback await mongoose.connection.transaction(async (session) => { await User.create([{ name: 'Alice' }], { session }); await Account.create([{ userId: user._id }], { session }); }); ``` ## Indexing ```typescript // Single field index userSchema.index({ email: 1 }); // Compound index userSchema.index({ role: 1, createdAt: -1 }); // Unique index userSchema.index({ email: 1 }, { unique: true }); // Partial index userSchema.index( { email: 1 }, { partialFilterExpression: { isActive: true } } ); // TTL index (auto-delete after time) sessionSchema.index({ createdAt: 1 }, { expireAfterSeconds: 3600 }); // Text index for search postSchema.index({ title: 'text', content: 'text' }); // Geospatial index locationSchema.index({ coordinates: '2dsphere' }); // Check indexes const indexes = await User.collection.getIndexes(); ``` ## Performance Tips ```typescript // Use lean() for read-only queries const users = await User.find().lean(); // Select only needed fields const users = await User.find().select('name email'); // Use cursor for large datasets const cursor = User.find().cursor(); for await (const user of cursor) { // Process one at a time } // Bulk operations const bulkOps = [ { insertOne: { document: { name: 'User 1' } } }, { updateOne: { filter: { _id: id1 }, update: { $set: { name: 'Updated' } } } }, { deleteOne: { filter: { _id: id2 } } }, ]; await User.bulkWrite(bulkOps); // Explain query const explanation = await User.find({ role: 'admin' }).explain('executionStats'); ``` ## MongoDB Atlas ```typescript // Atlas connection string const uri = 'mongodb+srv://user:password@cluster.mongodb.net/dbname?retryWrites=true&w=majority'; // Atlas Search (full-text search) const results = await Product.aggregate([ { $search: { index: 'default', text: { query: 'wireless headphones', path: ['name', 'description'], fuzzy: { maxEdits: 1 }, }, }}, { $project: { name: 1, score: { $meta: 'searchScore' }, }}, ]); // Atlas Vector Search const results = await Product.aggregate([ { $vectorSearch: { index: 'vector_index', path: 'embedding', queryVector: [0.1, 0.2, ...], numCandidates: 100, limit: 10, }}, ]); ``` ## Resources - **MongoDB Docs**: https://www.mongodb.com/docs/ - **Mongoose Docs**: https://mongoosejs.com/docs/ - **MongoDB University**: https://learn.mongodb.com/ - **Atlas Docs**: https://www.mongodb.com/docs/atlas/