MongoDB & Mongoose
📖 Concept
MongoDB is a NoSQL document database that stores data as JSON-like documents (BSON). Mongoose is the most popular ODM (Object Document Mapper) for MongoDB in Node.js.
Why MongoDB for Node.js?
- Documents are JSON-like — natural fit for JavaScript
- Flexible schema — great for rapid development and evolving data models
- Horizontal scaling with sharding
- Aggregation pipeline for complex queries
Mongoose key concepts:
- Schema — Defines document structure, types, validation, defaults
- Model — Compiled schema → constructor for creating/querying documents
- Document — An instance of a Model (a single database record)
- Middleware (hooks) — pre/post hooks for save, validate, remove, find
Schema types:
String, Number, Boolean, Date, Buffer, ObjectId, Array, Map, Mixed
Connection patterns:
- Single connection —
mongoose.connect(uri)— most apps - Connection pooling — Mongoose maintains a pool internally (default 5)
- Multiple connections —
mongoose.createConnection(uri)— multi-database
🏠 Real-world analogy: MongoDB is a filing cabinet where each drawer (collection) holds folders (documents). Unlike SQL's rigid spreadsheets, each folder can have different contents. Mongoose is the secretary who enforces filing rules (schema) and retrieves documents for you.
💻 Code Example
1// MongoDB with Mongoose — Complete Setup23const mongoose = require("mongoose");45// 1. Connection with best practices6async function connectDB() {7 try {8 await mongoose.connect(process.env.MONGODB_URI || "mongodb://localhost:27017/myapp", {9 maxPoolSize: 10,10 serverSelectionTimeoutMS: 5000,11 socketTimeoutMS: 45000,12 });13 console.log("MongoDB connected");14 } catch (err) {15 console.error("MongoDB connection error:", err);16 process.exit(1);17 }18}1920// Connection events21mongoose.connection.on("error", (err) => console.error("MongoDB error:", err));22mongoose.connection.on("disconnected", () => console.log("MongoDB disconnected"));2324// 2. Schema definition with validation25const userSchema = new mongoose.Schema(26 {27 name: {28 type: String,29 required: [true, "Name is required"],30 trim: true,31 minlength: [2, "Name must be at least 2 characters"],32 maxlength: [50, "Name cannot exceed 50 characters"],33 },34 email: {35 type: String,36 required: [true, "Email is required"],37 unique: true,38 lowercase: true,39 trim: true,40 match: [/^[^\s@]+@[^\s@]+\.[^\s@]+$/, "Invalid email format"],41 },42 password: {43 type: String,44 required: true,45 minlength: 8,46 select: false, // Don't include in queries by default47 },48 role: {49 type: String,50 enum: ["user", "admin", "moderator"],51 default: "user",52 },53 profile: {54 bio: { type: String, maxlength: 500 },55 avatar: String,56 social: {57 twitter: String,58 github: String,59 },60 },61 posts: [{ type: mongoose.Schema.Types.ObjectId, ref: "Post" }],62 isActive: { type: Boolean, default: true },63 loginCount: { type: Number, default: 0 },64 lastLogin: Date,65 },66 {67 timestamps: true, // Adds createdAt and updatedAt68 toJSON: { virtuals: true },69 toObject: { virtuals: true },70 }71);7273// 3. Virtual fields (computed, not stored in DB)74userSchema.virtual("displayName").get(function () {75 return this.name.charAt(0).toUpperCase() + this.name.slice(1);76});7778// 4. Instance methods79userSchema.methods.comparePassword = async function (candidatePassword) {80 const bcrypt = require("bcryptjs");81 return bcrypt.compare(candidatePassword, this.password);82};8384// 5. Static methods (on the Model)85userSchema.statics.findByEmail = function (email) {86 return this.findOne({ email: email.toLowerCase() });87};8889userSchema.statics.findActiveUsers = function () {90 return this.find({ isActive: true }).sort("-createdAt");91};9293// 6. Middleware (hooks)94userSchema.pre("save", async function (next) {95 // Hash password before saving96 if (this.isModified("password")) {97 const bcrypt = require("bcryptjs");98 this.password = await bcrypt.hash(this.password, 12);99 }100 next();101});102103// 7. Indexes104userSchema.index({ email: 1 }, { unique: true });105userSchema.index({ name: "text", "profile.bio": "text" }); // Text search106userSchema.index({ createdAt: -1 }); // Sort optimization107108// 8. Create model109const User = mongoose.model("User", userSchema);110111// 9. CRUD Operations112async function userCrudExamples() {113 // CREATE114 const user = await User.create({115 name: "Alice",116 email: "alice@example.com",117 password: "securepass123",118 });119120 // READ121 const allUsers = await User.find({ isActive: true })122 .select("name email role")123 .sort("-createdAt")124 .limit(10)125 .lean(); // Returns plain objects (faster)126127 const singleUser = await User.findById(user._id);128 const byEmail = await User.findByEmail("alice@example.com");129130 // UPDATE131 await User.findByIdAndUpdate(132 user._id,133 { $set: { role: "admin" }, $inc: { loginCount: 1 } },134 { new: true, runValidators: true }135 );136137 // DELETE138 await User.findByIdAndDelete(user._id);139140 // Aggregation141 const stats = await User.aggregate([142 { $match: { isActive: true } },143 { $group: { _id: "$role", count: { $sum: 1 }, avgLogins: { $avg: "$loginCount" } } },144 { $sort: { count: -1 } },145 ]);146147 // Population (join-like)148 const userWithPosts = await User.findById(user._id).populate("posts");149}150151module.exports = { User, connectDB };
🏋️ Practice Exercise
Exercises:
- Design a Mongoose schema for a blog with Users, Posts, and Comments — include validation and relationships
- Implement CRUD operations with proper error handling for a "Product" model
- Add pre-save middleware to hash passwords and post-save middleware to send welcome emails
- Use the Aggregation Pipeline to generate analytics: posts per user, average comment count, etc.
- Implement pagination with
.skip()and.limit()— then compare with cursor-based pagination - Add text indexes and implement full-text search across multiple fields
⚠️ Common Mistakes
Not handling
mongoose.connect()errors — a failed connection should crash the app at startup, not silently failUsing
findOne()without awaiting — Mongoose queries return thenables, not Promises; alwaysawaitor call.exec()Not using
.lean()for read-only queries — without it, Mongoose wraps results in full document instances, wasting memoryForgetting
{ new: true }infindByIdAndUpdate— without it, the method returns the OLD document before the updateNot adding indexes for frequently queried fields — unindexed queries do full collection scans, which are extremely slow at scale
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for MongoDB & Mongoose. Login to unlock this feature.