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:

  1. Schema — Defines document structure, types, validation, defaults
  2. Model — Compiled schema → constructor for creating/querying documents
  3. Document — An instance of a Model (a single database record)
  4. Middleware (hooks) — pre/post hooks for save, validate, remove, find

Schema types: String, Number, Boolean, Date, Buffer, ObjectId, Array, Map, Mixed

Connection patterns:

  • Single connectionmongoose.connect(uri) — most apps
  • Connection pooling — Mongoose maintains a pool internally (default 5)
  • Multiple connectionsmongoose.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

codeTap to expand ⛶
1// MongoDB with Mongoose — Complete Setup
2
3const mongoose = require("mongoose");
4
5// 1. Connection with best practices
6async 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}
19
20// Connection events
21mongoose.connection.on("error", (err) => console.error("MongoDB error:", err));
22mongoose.connection.on("disconnected", () => console.log("MongoDB disconnected"));
23
24// 2. Schema definition with validation
25const 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 default
47 },
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 updatedAt
68 toJSON: { virtuals: true },
69 toObject: { virtuals: true },
70 }
71);
72
73// 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});
77
78// 4. Instance methods
79userSchema.methods.comparePassword = async function (candidatePassword) {
80 const bcrypt = require("bcryptjs");
81 return bcrypt.compare(candidatePassword, this.password);
82};
83
84// 5. Static methods (on the Model)
85userSchema.statics.findByEmail = function (email) {
86 return this.findOne({ email: email.toLowerCase() });
87};
88
89userSchema.statics.findActiveUsers = function () {
90 return this.find({ isActive: true }).sort("-createdAt");
91};
92
93// 6. Middleware (hooks)
94userSchema.pre("save", async function (next) {
95 // Hash password before saving
96 if (this.isModified("password")) {
97 const bcrypt = require("bcryptjs");
98 this.password = await bcrypt.hash(this.password, 12);
99 }
100 next();
101});
102
103// 7. Indexes
104userSchema.index({ email: 1 }, { unique: true });
105userSchema.index({ name: "text", "profile.bio": "text" }); // Text search
106userSchema.index({ createdAt: -1 }); // Sort optimization
107
108// 8. Create model
109const User = mongoose.model("User", userSchema);
110
111// 9. CRUD Operations
112async function userCrudExamples() {
113 // CREATE
114 const user = await User.create({
115 name: "Alice",
116 email: "alice@example.com",
117 password: "securepass123",
118 });
119
120 // READ
121 const allUsers = await User.find({ isActive: true })
122 .select("name email role")
123 .sort("-createdAt")
124 .limit(10)
125 .lean(); // Returns plain objects (faster)
126
127 const singleUser = await User.findById(user._id);
128 const byEmail = await User.findByEmail("alice@example.com");
129
130 // UPDATE
131 await User.findByIdAndUpdate(
132 user._id,
133 { $set: { role: "admin" }, $inc: { loginCount: 1 } },
134 { new: true, runValidators: true }
135 );
136
137 // DELETE
138 await User.findByIdAndDelete(user._id);
139
140 // Aggregation
141 const stats = await User.aggregate([
142 { $match: { isActive: true } },
143 { $group: { _id: "$role", count: { $sum: 1 }, avgLogins: { $avg: "$loginCount" } } },
144 { $sort: { count: -1 } },
145 ]);
146
147 // Population (join-like)
148 const userWithPosts = await User.findById(user._id).populate("posts");
149}
150
151module.exports = { User, connectDB };

🏋️ Practice Exercise

Exercises:

  1. Design a Mongoose schema for a blog with Users, Posts, and Comments — include validation and relationships
  2. Implement CRUD operations with proper error handling for a "Product" model
  3. Add pre-save middleware to hash passwords and post-save middleware to send welcome emails
  4. Use the Aggregation Pipeline to generate analytics: posts per user, average comment count, etc.
  5. Implement pagination with .skip() and .limit() — then compare with cursor-based pagination
  6. 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 fail

  • Using findOne() without awaiting — Mongoose queries return thenables, not Promises; always await or call .exec()

  • Not using .lean() for read-only queries — without it, Mongoose wraps results in full document instances, wasting memory

  • Forgetting { new: true } in findByIdAndUpdate — without it, the method returns the OLD document before the update

  • Not 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.