Master MongoDB with Node.js using Mongoose ODM. Learn schema design, validation, CRUD operations, population, and aggregation pipelines.
Master MongoDB with Node.js using Mongoose ODM. Learn schema design, validation, CRUD operations, population, and aggregation pipelines.
Understand MongoDB architecture and document-based storage
Set up MongoDB with Node.js using Mongoose ODM
Design efficient schemas with validation
Implement CRUD operations with error handling
Master population and references between documents
Build complex aggregation pipelines for data processing
MongoDB is a NoSQL document database that stores data in flexible, JSON-like documents called BSON (Binary JSON).
Learn about MongoDB Installation & Connection.
1# Install MongoDB locally (macOS)
2brew tap mongodb/brew
3brew install mongodb-community
4
5# Start MongoDB service
6brew services start mongodb/brew/mongodb-community
7
8# Install Node.js dependencies
9npm install mongoose dotenv
10npm install --save-dev @types/mongoose1const mongoose = require('mongoose');
2require('dotenv').config();
3
4class DatabaseConnection {
5 constructor() {
6 this.connection = null;
7 this.isConnected = false;
8 }
9
10 async connect() {
11 try {
12 // Connection options for production
13 const options = {
14 useNewUrlParser: true,
15 useUnifiedTopology: true,
16 maxPoolSize: 10, // Maintain up to 10 socket connections
17 serverSelectionTimeoutMS: 5000, // Keep trying to send operations for 5 seconds
18 socketTimeoutMS: 45000, // Close sockets after 45 seconds of inactivity
19 family: 4, // Use IPv4, skip trying IPv6
20 retryWrites: true,
21 w: 'majority' // Write concern
22 };
23
24 this.connection = await mongoose.connect(
25 process.env.MONGODB_URI || 'mongodb://localhost:27017/myapp',
26 options
27 );
28
29 this.isConnected = true;
30 console.log('✅ MongoDB connected successfully');
31
32 // Handle connection events
33 mongoose.connection.on('disconnected', () => {
34 console.log('❌ MongoDB disconnected');
35 this.isConnected = false;
36 });
37
38 mongoose.connection.on('error', (error) => {
39 console.error('❌ MongoDB connection error:', error);
40 this.isConnected = false;
41 });
42
43 // Graceful shutdown
44 process.on('SIGINT', async () => {
45 await this.disconnect();
46 process.exit(0);
47 });
48
49 } catch (error) {
50 console.error('❌ MongoDB connection failed:', error);
51 throw error;
52 }
53 }
54
55 async disconnect() {
56 if (this.connection) {
57 await mongoose.connection.close();
58 console.log('🔌 MongoDB disconnected gracefully');
59 }
60 }
61
62 getStatus() {
63 return {
64 isConnected: this.isConnected,
65 readyState: mongoose.connection.readyState,
66 host: mongoose.connection.host,
67 port: mongoose.connection.port,
68 name: mongoose.connection.name
69 };
70 }
71}
72
73module.exports = new DatabaseConnection();Mongoose provides a schema-based solution to model application data with built-in type casting, validation, and business logic hooks.
1const mongoose = require('mongoose');
2const bcrypt = require('bcryptjs');
3const validator = require('validator');
4
5// Define subdocument schemas
6const addressSchema = new mongoose.Schema({
7 street: {
8 type: String,
9 required: [true, 'Street address is required'],
10 trim: true,
11 maxlength: [100, 'Street address cannot exceed 100 characters']
12 },
13 city: {
14 type: String,
15 required: [true, 'City is required'],
16 trim: true,
17 maxlength: [50, 'City name cannot exceed 50 characters']
18 },
19 state: {
20 type: String,
21 required: [true, 'State is required'],
22 trim: true,
23 maxlength: [50, 'State name cannot exceed 50 characters']
24 },
25 zipCode: {
26 type: String,
27 required: [true, 'ZIP code is required'],
28 validate: {
29 validator: function(v) {
30 return /^\\d{5}(-\\d{4})?$/.test(v);
31 },
32 message: 'Invalid ZIP code format'
33 }
34 },
35 country: {
36 type: String,
37 required: [true, 'Country is required'],
38 default: 'United States',
39 trim: true
40 }
41}, { _id: false }); // Disable _id for subdocuments
42
43// Main User schema with comprehensive validation
44const userSchema = new mongoose.Schema({
45 // Basic Information
46 firstName: {
47 type: String,
48 required: [true, 'First name is required'],
49 trim: true,
50 minlength: [2, 'First name must be at least 2 characters'],
51 maxlength: [30, 'First name cannot exceed 30 characters'],
52 validate: {
53 validator: function(v) {
54 return /^[a-zA-Z\\s]+$/.test(v);
55 },
56 message: 'First name can only contain letters and spaces'
57 }
58 },
59
60 lastName: {
61 type: String,
62 required: [true, 'Last name is required'],
63 trim: true,
64 minlength: [2, 'Last name must be at least 2 characters'],
65 maxlength: [30, 'Last name cannot exceed 30 characters'],
66 validate: {
67 validator: function(v) {
68 return /^[a-zA-Z\\s]+$/.test(v);
69 },
70 message: 'Last name can only contain letters and spaces'
71 }
72 },
73
74 // Authentication
75 email: {
76 type: String,
77 required: [true, 'Email is required'],
78 unique: true,
79 lowercase: true,
80 trim: true,
81 validate: {
82 validator: validator.isEmail,
83 message: 'Please provide a valid email address'
84 },
85 index: true // Create index for faster queries
86 },
87
88 password: {
89 type: String,
90 required: [true, 'Password is required'],
91 minlength: [8, 'Password must be at least 8 characters'],
92 validate: {
93 validator: function(v) {
94 // At least one uppercase, one lowercase, one digit, one special char
95 return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]/.test(v);
96 },
97 message: 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character'
98 },
99 select: false // Don't include password in queries by default
100 },
101
102 // Profile Information
103 avatar: {
104 type: String,
105 default: function() {
106 // Generate Gravatar URL based on email
107 const crypto = require('crypto');
108 const hash = crypto.createHash('md5').update(this.email.toLowerCase()).digest('hex');
109 return \`https://www.gravatar.com/avatar/\${hash}?d=identicon&s=200\`;
110 }
111 },
112
113 dateOfBirth: {
114 type: Date,
115 validate: {
116 validator: function(v) {
117 const today = new Date();
118 const age = today.getFullYear() - v.getFullYear();
119 return age >= 13 && age <= 120; // Age restrictions
120 },
121 message: 'User must be between 13 and 120 years old'
122 }
123 },
124
125 phoneNumber: {
126 type: String,
127 validate: {
128 validator: function(v) {
129 return !v || validator.isMobilePhone(v, 'any');
130 },
131 message: 'Please provide a valid phone number'
132 }
133 },
134
135 // Address using subdocument
136 address: addressSchema,
137
138 // Account Status
139 isActive: {
140 type: Boolean,
141 default: true
142 },
143
144 isEmailVerified: {
145 type: Boolean,
146 default: false
147 },
148
149 // Role-based access control
150 role: {
151 type: String,
152 enum: {
153 values: ['user', 'admin', 'moderator', 'premium'],
154 message: 'Role must be one of: user, admin, moderator, premium'
155 },
156 default: 'user'
157 },
158
159 // Preferences
160 preferences: {
161 newsletter: {
162 type: Boolean,
163 default: true
164 },
165 notifications: {
166 email: { type: Boolean, default: true },
167 sms: { type: Boolean, default: false },
168 push: { type: Boolean, default: true }
169 },
170 theme: {
171 type: String,
172 enum: ['light', 'dark', 'auto'],
173 default: 'auto'
174 },
175 language: {
176 type: String,
177 default: 'en',
178 validate: {
179 validator: function(v) {
180 return /^[a-z]{2}$/.test(v); // ISO 639-1 language codes
181 },
182 message: 'Language must be a valid ISO 639-1 code'
183 }
184 }
185 },
186
187 // Security
188 lastLogin: Date,
189 loginAttempts: {
190 type: Number,
191 default: 0,
192 max: 5
193 },
194 lockUntil: Date,
195 passwordResetToken: String,
196 passwordResetExpires: Date,
197 emailVerificationToken: String,
198 emailVerificationExpires: Date,
199
200 // Social Login
201 googleId: String,
202 facebookId: String,
203 githubId: String,
204
205 // Analytics
206 registrationSource: {
207 type: String,
208 enum: ['web', 'mobile', 'api', 'social', 'referral'],
209 default: 'web'
210 },
211 referralCode: String,
212 metadata: {
213 ipAddress: String,
214 userAgent: String,
215 deviceInfo: mongoose.Schema.Types.Mixed
216 }
217
218}, {
219 timestamps: true, // Automatically add createdAt and updatedAt
220 versionKey: false, // Disable __v version key
221 collection: 'users' // Explicit collection name
222});
223
224// Compound indexes for better query performance
225userSchema.index({ email: 1, isActive: 1 });
226userSchema.index({ role: 1, isActive: 1 });
227userSchema.index({ createdAt: -1 });
228
229// Virtual properties
230userSchema.virtual('fullName').get(function() {
231 return \`\${this.firstName} \${this.lastName}\`;
232});
233
234userSchema.virtual('age').get(function() {
235 if (!this.dateOfBirth) return null;
236 const today = new Date();
237 const birthDate = new Date(this.dateOfBirth);
238 let age = today.getFullYear() - birthDate.getFullYear();
239 const monthDiff = today.getMonth() - birthDate.getMonth();
240
241 if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
242 age--;
243 }
244 return age;
245});
246
247userSchema.virtual('isAccountLocked').get(function() {
248 return !!(this.lockUntil && this.lockUntil > Date.now());
249});
250
251// Include virtuals when converting to JSON
252userSchema.set('toJSON', {
253 virtuals: true,
254 transform: function(doc, ret) {
255 delete ret.password;
256 delete ret.passwordResetToken;
257 delete ret.emailVerificationToken;
258 delete ret.loginAttempts;
259 delete ret.lockUntil;
260 return ret;
261 }
262});
263
264// Pre-save middleware for password hashing
265userSchema.pre('save', async function(next) {
266 // Only hash the password if it has been modified (or is new)
267 if (!this.isModified('password')) return next();
268
269 try {
270 // Hash password with cost of 12
271 const saltRounds = 12;
272 this.password = await bcrypt.hash(this.password, saltRounds);
273 next();
274 } catch (error) {
275 next(error);
276 }
277});
278
279// Instance methods
280userSchema.methods.comparePassword = async function(candidatePassword) {
281 if (!this.password) return false;
282 return bcrypt.compare(candidatePassword, this.password);
283};
284
285userSchema.methods.generateAuthToken = function() {
286 const jwt = require('jsonwebtoken');
287 return jwt.sign(
288 {
289 userId: this._id,
290 email: this.email,
291 role: this.role
292 },
293 process.env.JWT_SECRET,
294 { expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
295 );
296};
297
298userSchema.methods.incrementLoginAttempts = function() {
299 // If we have a previous lock that has expired, restart at 1
300 if (this.lockUntil && this.lockUntil < Date.now()) {
301 return this.updateOne({
302 $unset: { lockUntil: 1 },
303 $set: { loginAttempts: 1 }
304 });
305 }
306
307 const updates = { $inc: { loginAttempts: 1 } };
308
309 // Check if we need to lock the account
310 if (this.loginAttempts + 1 >= 5 && !this.isAccountLocked) {
311 updates.$set = { lockUntil: Date.now() + 2 * 60 * 60 * 1000 }; // Lock for 2 hours
312 }
313
314 return this.updateOne(updates);
315};
316
317userSchema.methods.resetLoginAttempts = function() {
318 return this.updateOne({
319 $unset: { loginAttempts: 1, lockUntil: 1 }
320 });
321};
322
323// Static methods
324userSchema.statics.findActiveUsers = function() {
325 return this.find({ isActive: true });
326};
327
328userSchema.statics.findByRole = function(role) {
329 return this.find({ role, isActive: true });
330};
331
332userSchema.statics.search = function(query, options = {}) {
333 const {
334 page = 1,
335 limit = 10,
336 sortBy = 'createdAt',
337 sortOrder = 'desc'
338 } = options;
339
340 const searchQuery = {
341 $and: [
342 { isActive: true },
343 {
344 $or: [
345 { firstName: { $regex: query, $options: 'i' } },
346 { lastName: { $regex: query, $options: 'i' } },
347 { email: { $regex: query, $options: 'i' } }
348 ]
349 }
350 ]
351 };
352
353 const skip = (page - 1) * limit;
354 const sort = { [sortBy]: sortOrder === 'desc' ? -1 : 1 };
355
356 return this.find(searchQuery)
357 .sort(sort)
358 .skip(skip)
359 .limit(limit);
360};
361
362// Export the model
363module.exports = mongoose.model('User', userSchema);Implement robust CRUD operations with comprehensive error handling and validation.
1const User = require('../models/User');
2const mongoose = require('mongoose');
3
4class UserService {
5 // Create a new user
6 async createUser(userData) {
7 try {
8 // Check if user already exists
9 const existingUser = await User.findOne({
10 email: userData.email.toLowerCase()
11 });
12
13 if (existingUser) {
14 throw new Error('User with this email already exists');
15 }
16
17 // Create new user
18 const user = new User(userData);
19 await user.save();
20
21 // Generate auth token
22 const token = user.generateAuthToken();
23
24 return {
25 success: true,
26 user: user.toJSON(),
27 token,
28 message: 'User created successfully'
29 };
30
31 } catch (error) {
32 if (error.name === 'ValidationError') {
33 const validationErrors = Object.values(error.errors).map(err => ({
34 field: err.path,
35 message: err.message
36 }));
37
38 throw new Error(\`Validation failed: \${validationErrors.map(e => e.message).join(', ')}\`);
39 }
40
41 if (error.code === 11000) {
42 const field = Object.keys(error.keyValue)[0];
43 throw new Error(\`\${field} already exists\`);
44 }
45
46 throw error;
47 }
48 }
49
50 // Get user by ID with population
51 async getUserById(userId, includePassword = false) {
52 try {
53 if (!mongoose.Types.ObjectId.isValid(userId)) {
54 throw new Error('Invalid user ID format');
55 }
56
57 let query = User.findById(userId);
58
59 if (includePassword) {
60 query = query.select('+password');
61 }
62
63 const user = await query.exec();
64
65 if (!user) {
66 throw new Error('User not found');
67 }
68
69 if (!user.isActive) {
70 throw new Error('User account is deactivated');
71 }
72
73 return {
74 success: true,
75 user: user.toJSON()
76 };
77
78 } catch (error) {
79 throw error;
80 }
81 }
82
83 // Update user with validation
84 async updateUser(userId, updateData, options = {}) {
85 try {
86 if (!mongoose.Types.ObjectId.isValid(userId)) {
87 throw new Error('Invalid user ID format');
88 }
89
90 // Remove sensitive fields from update
91 const sensitiveFields = ['password', 'role', 'isActive', 'emailVerificationToken'];
92 sensitiveFields.forEach(field => delete updateData[field]);
93
94 const updateOptions = {
95 new: true, // Return updated document
96 runValidators: true, // Run schema validation
97 context: 'query', // Set context for validation
98 ...options
99 };
100
101 const user = await User.findByIdAndUpdate(
102 userId,
103 { $set: updateData },
104 updateOptions
105 );
106
107 if (!user) {
108 throw new Error('User not found');
109 }
110
111 return {
112 success: true,
113 user: user.toJSON(),
114 message: 'User updated successfully'
115 };
116
117 } catch (error) {
118 if (error.name === 'ValidationError') {
119 const validationErrors = Object.values(error.errors).map(err => ({
120 field: err.path,
121 message: err.message
122 }));
123
124 throw new Error(\`Validation failed: \${validationErrors.map(e => e.message).join(', ')}\`);
125 }
126
127 throw error;
128 }
129 }
130
131 // Delete user (soft delete)
132 async deleteUser(userId) {
133 try {
134 if (!mongoose.Types.ObjectId.isValid(userId)) {
135 throw new Error('Invalid user ID format');
136 }
137
138 const user = await User.findByIdAndUpdate(
139 userId,
140 {
141 isActive: false,
142 email: \`deleted_\${Date.now()}_\${user.email}\` // Prevent email conflicts
143 },
144 { new: true }
145 );
146
147 if (!user) {
148 throw new Error('User not found');
149 }
150
151 return {
152 success: true,
153 message: 'User deleted successfully'
154 };
155
156 } catch (error) {
157 throw error;
158 }
159 }
160
161 // Get users with pagination and filtering
162 async getUsers(filters = {}, pagination = {}) {
163 try {
164 const {
165 page = 1,
166 limit = 10,
167 sortBy = 'createdAt',
168 sortOrder = 'desc',
169 search = ''
170 } = pagination;
171
172 // Build query
173 let query = { isActive: true };
174
175 // Apply filters
176 if (filters.role) {
177 query.role = filters.role;
178 }
179
180 if (filters.isEmailVerified !== undefined) {
181 query.isEmailVerified = filters.isEmailVerified;
182 }
183
184 if (filters.dateRange) {
185 query.createdAt = {
186 $gte: new Date(filters.dateRange.start),
187 $lte: new Date(filters.dateRange.end)
188 };
189 }
190
191 // Apply search
192 if (search) {
193 query.$or = [
194 { firstName: { $regex: search, $options: 'i' } },
195 { lastName: { $regex: search, $options: 'i' } },
196 { email: { $regex: search, $options: 'i' } }
197 ];
198 }
199
200 // Calculate pagination
201 const skip = (page - 1) * limit;
202 const sort = { [sortBy]: sortOrder === 'desc' ? -1 : 1 };
203
204 // Execute queries
205 const [users, totalCount] = await Promise.all([
206 User.find(query)
207 .sort(sort)
208 .skip(skip)
209 .limit(limit)
210 .select('-password')
211 .lean(),
212 User.countDocuments(query)
213 ]);
214
215 const totalPages = Math.ceil(totalCount / limit);
216
217 return {
218 success: true,
219 users,
220 pagination: {
221 currentPage: page,
222 totalPages,
223 totalCount,
224 hasNextPage: page < totalPages,
225 hasPrevPage: page > 1,
226 limit
227 }
228 };
229
230 } catch (error) {
231 throw error;
232 }
233 }
234
235 // Bulk operations
236 async bulkCreateUsers(usersData) {
237 try {
238 const results = {
239 successful: [],
240 failed: []
241 };
242
243 for (const userData of usersData) {
244 try {
245 const result = await this.createUser(userData);
246 results.successful.push(result.user);
247 } catch (error) {
248 results.failed.push({
249 userData,
250 error: error.message
251 });
252 }
253 }
254
255 return {
256 success: true,
257 results,
258 summary: {
259 total: usersData.length,
260 successful: results.successful.length,
261 failed: results.failed.length
262 }
263 };
264
265 } catch (error) {
266 throw error;
267 }
268 }
269
270 // User statistics
271 async getUserStats() {
272 try {
273 const stats = await User.aggregate([
274 {
275 $facet: {
276 totalUsers: [
277 { $match: { isActive: true } },
278 { $count: "count" }
279 ],
280 usersByRole: [
281 { $match: { isActive: true } },
282 { $group: { _id: "$role", count: { $sum: 1 } } }
283 ],
284 usersByMonth: [
285 { $match: { isActive: true } },
286 {
287 $group: {
288 _id: {
289 year: { $year: "$createdAt" },
290 month: { $month: "$createdAt" }
291 },
292 count: { $sum: 1 }
293 }
294 },
295 { $sort: { "_id.year": -1, "_id.month": -1 } },
296 { $limit: 12 }
297 ],
298 emailVerificationStats: [
299 { $match: { isActive: true } },
300 {
301 $group: {
302 _id: "$isEmailVerified",
303 count: { $sum: 1 }
304 }
305 }
306 ]
307 }
308 }
309 ]);
310
311 return {
312 success: true,
313 stats: stats[0]
314 };
315
316 } catch (error) {
317 throw error;
318 }
319 }
320}
321
322module.exports = new UserService();MongoDB references allow you to store relationships between documents and populate them on demand.
1const mongoose = require('mongoose');
2
3// Comment subdocument schema
4const commentSchema = new mongoose.Schema({
5 author: {
6 type: mongoose.Schema.Types.ObjectId,
7 ref: 'User',
8 required: true
9 },
10 content: {
11 type: String,
12 required: [true, 'Comment content is required'],
13 trim: true,
14 maxlength: [500, 'Comment cannot exceed 500 characters']
15 },
16 likes: [{
17 user: {
18 type: mongoose.Schema.Types.ObjectId,
19 ref: 'User'
20 },
21 likedAt: {
22 type: Date,
23 default: Date.now
24 }
25 }],
26 isEdited: {
27 type: Boolean,
28 default: false
29 },
30 editedAt: Date
31}, {
32 timestamps: true
33});
34
35// Tag schema for categories
36const tagSchema = new mongoose.Schema({
37 name: {
38 type: String,
39 required: true,
40 unique: true,
41 lowercase: true,
42 trim: true
43 },
44 description: String,
45 color: {
46 type: String,
47 default: '#007bff'
48 }
49}, { timestamps: true });
50
51// Main Post schema with references
52const postSchema = new mongoose.Schema({
53 title: {
54 type: String,
55 required: [true, 'Post title is required'],
56 trim: true,
57 maxlength: [100, 'Title cannot exceed 100 characters']
58 },
59
60 content: {
61 type: String,
62 required: [true, 'Post content is required'],
63 trim: true,
64 minlength: [10, 'Content must be at least 10 characters']
65 },
66
67 excerpt: {
68 type: String,
69 maxlength: [200, 'Excerpt cannot exceed 200 characters']
70 },
71
72 // Reference to User model
73 author: {
74 type: mongoose.Schema.Types.ObjectId,
75 ref: 'User',
76 required: true,
77 index: true
78 },
79
80 // Array of references to Tag model
81 tags: [{
82 type: mongoose.Schema.Types.ObjectId,
83 ref: 'Tag'
84 }],
85
86 // Embedded comments
87 comments: [commentSchema],
88
89 // Post metadata
90 status: {
91 type: String,
92 enum: ['draft', 'published', 'archived'],
93 default: 'draft'
94 },
95
96 publishedAt: Date,
97
98 // Social engagement
99 likes: [{
100 user: {
101 type: mongoose.Schema.Types.ObjectId,
102 ref: 'User'
103 },
104 likedAt: {
105 type: Date,
106 default: Date.now
107 }
108 }],
109
110 views: {
111 type: Number,
112 default: 0
113 },
114
115 // Featured image
116 featuredImage: {
117 url: String,
118 alt: String,
119 caption: String
120 },
121
122 // SEO fields
123 seo: {
124 metaTitle: String,
125 metaDescription: String,
126 slug: {
127 type: String,
128 unique: true,
129 sparse: true // Allow null values but ensure uniqueness when present
130 }
131 }
132
133}, {
134 timestamps: true,
135 versionKey: false
136});
137
138// Indexes for better performance
139postSchema.index({ author: 1, status: 1 });
140postSchema.index({ tags: 1 });
141postSchema.index({ publishedAt: -1 });
142postSchema.index({ 'seo.slug': 1 });
143postSchema.index({ title: 'text', content: 'text' }); // Text search
144
145// Virtual for comment count
146postSchema.virtual('commentCount').get(function() {
147 return this.comments.length;
148});
149
150// Virtual for like count
151postSchema.virtual('likeCount').get(function() {
152 return this.likes.length;
153});
154
155// Pre-save middleware to generate slug and excerpt
156postSchema.pre('save', function(next) {
157 // Generate slug from title if not provided
158 if (!this.seo.slug && this.title) {
159 this.seo.slug = this.title
160 .toLowerCase()
161 .replace(/[^a-z0-9]+/g, '-')
162 .replace(/^-+|-+$/g, '');
163 }
164
165 // Generate excerpt if not provided
166 if (!this.excerpt && this.content) {
167 this.excerpt = this.content.substring(0, 150) + '...';
168 }
169
170 // Set publishedAt when status changes to published
171 if (this.isModified('status') && this.status === 'published' && !this.publishedAt) {
172 this.publishedAt = new Date();
173 }
174
175 next();
176});
177
178// Static methods for population
179postSchema.statics.findWithAuthor = function(query = {}) {
180 return this.find(query)
181 .populate('author', 'firstName lastName avatar email')
182 .populate('tags', 'name color');
183};
184
185postSchema.statics.findPublishedPosts = function(options = {}) {
186 const { page = 1, limit = 10, tags = [] } = options;
187
188 let query = { status: 'published' };
189
190 if (tags.length > 0) {
191 query.tags = { $in: tags };
192 }
193
194 const skip = (page - 1) * limit;
195
196 return this.find(query)
197 .populate('author', 'firstName lastName avatar')
198 .populate('tags', 'name color')
199 .sort({ publishedAt: -1 })
200 .skip(skip)
201 .limit(limit);
202};
203
204// Instance methods
205postSchema.methods.addComment = function(userId, content) {
206 this.comments.push({
207 author: userId,
208 content: content
209 });
210 return this.save();
211};
212
213postSchema.methods.toggleLike = function(userId) {
214 const likeIndex = this.likes.findIndex(
215 like => like.user.toString() === userId.toString()
216 );
217
218 if (likeIndex > -1) {
219 this.likes.splice(likeIndex, 1);
220 } else {
221 this.likes.push({ user: userId });
222 }
223
224 return this.save();
225};
226
227postSchema.methods.incrementViews = function() {
228 return this.updateOne({ $inc: { views: 1 } });
229};
230
231// Create models
232const Tag = mongoose.model('Tag', tagSchema);
233const Post = mongoose.model('Post', postSchema);
234
235module.exports = { Post, Tag };MongoDB aggregation pipelines provide powerful data processing capabilities for complex queries and transformations.
1const { Post, Tag } = require('../models/Post');
2const User = require('../models/User');
3const mongoose = require('mongoose');
4
5class AnalyticsService {
6 // Advanced post analytics with aggregation
7 async getPostAnalytics(dateRange = {}) {
8 try {
9 const { startDate, endDate } = dateRange;
10
11 const matchStage = { status: 'published' };
12 if (startDate && endDate) {
13 matchStage.publishedAt = {
14 $gte: new Date(startDate),
15 $lte: new Date(endDate)
16 };
17 }
18
19 const analytics = await Post.aggregate([
20 // Stage 1: Match published posts in date range
21 { $match: matchStage },
22
23 // Stage 2: Lookup author information
24 {
25 $lookup: {
26 from: 'users',
27 localField: 'author',
28 foreignField: '_id',
29 as: 'authorInfo',
30 pipeline: [
31 { $project: { firstName: 1, lastName: 1, email: 1 } }
32 ]
33 }
34 },
35
36 // Stage 3: Lookup tag information
37 {
38 $lookup: {
39 from: 'tags',
40 localField: 'tags',
41 foreignField: '_id',
42 as: 'tagInfo'
43 }
44 },
45
46 // Stage 4: Add computed fields
47 {
48 $addFields: {
49 authorName: {
50 $concat: [
51 { $arrayElemAt: ['$authorInfo.firstName', 0] },
52 ' ',
53 { $arrayElemAt: ['$authorInfo.lastName', 0] }
54 ]
55 },
56 likeCount: { $size: '$likes' },
57 commentCount: { $size: '$comments' },
58 engagementScore: {
59 $add: [
60 { $multiply: [{ $size: '$likes' }, 1] },
61 { $multiply: [{ $size: '$comments' }, 2] },
62 { $multiply: ['$views', 0.1] }
63 ]
64 },
65 dayOfWeek: { $dayOfWeek: '$publishedAt' },
66 hourOfDay: { $hour: '$publishedAt' }
67 }
68 },
69
70 // Stage 5: Create comprehensive facets
71 {
72 $facet: {
73 // Overall statistics
74 overallStats: [
75 {
76 $group: {
77 _id: null,
78 totalPosts: { $sum: 1 },
79 totalViews: { $sum: '$views' },
80 totalLikes: { $sum: '$likeCount' },
81 totalComments: { $sum: '$commentCount' },
82 avgEngagement: { $avg: '$engagementScore' },
83 avgViewsPerPost: { $avg: '$views' },
84 avgLikesPerPost: { $avg: '$likeCount' },
85 avgCommentsPerPost: { $avg: '$commentCount' }
86 }
87 }
88 ],
89
90 // Top performing posts
91 topPosts: [
92 { $sort: { engagementScore: -1 } },
93 { $limit: 10 },
94 {
95 $project: {
96 title: 1,
97 authorName: 1,
98 views: 1,
99 likeCount: 1,
100 commentCount: 1,
101 engagementScore: 1,
102 publishedAt: 1
103 }
104 }
105 ],
106
107 // Posts by author
108 postsByAuthor: [
109 {
110 $group: {
111 _id: '$author',
112 authorName: { $first: '$authorName' },
113 postCount: { $sum: 1 },
114 totalViews: { $sum: '$views' },
115 totalLikes: { $sum: '$likeCount' },
116 totalComments: { $sum: '$commentCount' },
117 avgEngagement: { $avg: '$engagementScore' }
118 }
119 },
120 { $sort: { postCount: -1 } },
121 { $limit: 10 }
122 ],
123
124 // Posts by day of week
125 postsByDayOfWeek: [
126 {
127 $group: {
128 _id: '$dayOfWeek',
129 postCount: { $sum: 1 },
130 avgViews: { $avg: '$views' },
131 avgEngagement: { $avg: '$engagementScore' }
132 }
133 },
134 { $sort: { _id: 1 } },
135 {
136 $addFields: {
137 dayName: {
138 $switch: {
139 branches: [
140 { case: { $eq: ['$_id', 1] }, then: 'Sunday' },
141 { case: { $eq: ['$_id', 2] }, then: 'Monday' },
142 { case: { $eq: ['$_id', 3] }, then: 'Tuesday' },
143 { case: { $eq: ['$_id', 4] }, then: 'Wednesday' },
144 { case: { $eq: ['$_id', 5] }, then: 'Thursday' },
145 { case: { $eq: ['$_id', 6] }, then: 'Friday' },
146 { case: { $eq: ['$_id', 7] }, then: 'Saturday' }
147 ],
148 default: 'Unknown'
149 }
150 }
151 }
152 }
153 ],
154
155 // Posts by hour of day
156 postsByHour: [
157 {
158 $group: {
159 _id: '$hourOfDay',
160 postCount: { $sum: 1 },
161 avgViews: { $avg: '$views' },
162 avgEngagement: { $avg: '$engagementScore' }
163 }
164 },
165 { $sort: { _id: 1 } }
166 ],
167
168 // Tag performance
169 tagPerformance: [
170 { $unwind: '$tagInfo' },
171 {
172 $group: {
173 _id: '$tagInfo._id',
174 tagName: { $first: '$tagInfo.name' },
175 postCount: { $sum: 1 },
176 totalViews: { $sum: '$views' },
177 avgViews: { $avg: '$views' },
178 totalEngagement: { $sum: '$engagementScore' },
179 avgEngagement: { $avg: '$engagementScore' }
180 }
181 },
182 { $sort: { postCount: -1 } },
183 { $limit: 15 }
184 ],
185
186 // Monthly trends
187 monthlyTrends: [
188 {
189 $group: {
190 _id: {
191 year: { $year: '$publishedAt' },
192 month: { $month: '$publishedAt' }
193 },
194 postCount: { $sum: 1 },
195 totalViews: { $sum: '$views' },
196 totalLikes: { $sum: '$likeCount' },
197 totalComments: { $sum: '$commentCount' },
198 avgEngagement: { $avg: '$engagementScore' }
199 }
200 },
201 { $sort: { '_id.year': -1, '_id.month': -1 } },
202 { $limit: 12 },
203 {
204 $addFields: {
205 monthName: {
206 $switch: {
207 branches: [
208 { case: { $eq: ['$_id.month', 1] }, then: 'January' },
209 { case: { $eq: ['$_id.month', 2] }, then: 'February' },
210 { case: { $eq: ['$_id.month', 3] }, then: 'March' },
211 { case: { $eq: ['$_id.month', 4] }, then: 'April' },
212 { case: { $eq: ['$_id.month', 5] }, then: 'May' },
213 { case: { $eq: ['$_id.month', 6] }, then: 'June' },
214 { case: { $eq: ['$_id.month', 7] }, then: 'July' },
215 { case: { $eq: ['$_id.month', 8] }, then: 'August' },
216 { case: { $eq: ['$_id.month', 9] }, then: 'September' },
217 { case: { $eq: ['$_id.month', 10] }, then: 'October' },
218 { case: { $eq: ['$_id.month', 11] }, then: 'November' },
219 { case: { $eq: ['$_id.month', 12] }, then: 'December' }
220 ],
221 default: 'Unknown'
222 }
223 }
224 }
225 }
226 ]
227 }
228 }
229 ]);
230
231 return {
232 success: true,
233 analytics: analytics[0],
234 generatedAt: new Date()
235 };
236
237 } catch (error) {
238 throw new Error(\`Analytics generation failed: \${error.message}\`);
239 }
240 }
241
242 // User engagement analytics
243 async getUserEngagementAnalytics() {
244 try {
245 const engagement = await User.aggregate([
246 // Stage 1: Match active users
247 { $match: { isActive: true } },
248
249 // Stage 2: Lookup user posts
250 {
251 $lookup: {
252 from: 'posts',
253 localField: '_id',
254 foreignField: 'author',
255 as: 'posts',
256 pipeline: [
257 { $match: { status: 'published' } }
258 ]
259 }
260 },
261
262 // Stage 3: Lookup user comments
263 {
264 $lookup: {
265 from: 'posts',
266 localField: '_id',
267 foreignField: 'comments.author',
268 as: 'commentedPosts'
269 }
270 },
271
272 // Stage 4: Lookup user likes
273 {
274 $lookup: {
275 from: 'posts',
276 localField: '_id',
277 foreignField: 'likes.user',
278 as: 'likedPosts'
279 }
280 },
281
282 // Stage 5: Calculate engagement metrics
283 {
284 $addFields: {
285 postCount: { $size: '$posts' },
286 totalViews: { $sum: '$posts.views' },
287 totalLikes: { $sum: { $size: '$posts.likes' } },
288 totalComments: { $sum: { $size: '$posts.comments' } },
289 commentsGiven: { $size: '$commentedPosts' },
290 likesGiven: { $size: '$likedPosts' },
291 joinDays: {
292 $divide: [
293 { $subtract: [new Date(), '$createdAt'] },
294 1000 * 60 * 60 * 24
295 ]
296 }
297 }
298 },
299
300 // Stage 6: Calculate engagement score
301 {
302 $addFields: {
303 engagementScore: {
304 $add: [
305 { $multiply: ['$postCount', 10] },
306 { $multiply: ['$commentsGiven', 2] },
307 { $multiply: ['$likesGiven', 1] },
308 { $multiply: ['$totalViews', 0.1] }
309 ]
310 },
311 avgPostsPerDay: {
312 $cond: {
313 if: { $gt: ['$joinDays', 0] },
314 then: { $divide: ['$postCount', '$joinDays'] },
315 else: 0
316 }
317 }
318 }
319 },
320
321 // Stage 7: Create user segments
322 {
323 $addFields: {
324 userSegment: {
325 $switch: {
326 branches: [
327 {
328 case: { $gte: ['$engagementScore', 100] },
329 then: 'Highly Engaged'
330 },
331 {
332 case: { $gte: ['$engagementScore', 50] },
333 then: 'Moderately Engaged'
334 },
335 {
336 case: { $gte: ['$engagementScore', 10] },
337 then: 'Lightly Engaged'
338 }
339 ],
340 default: 'New/Inactive'
341 }
342 }
343 }
344 },
345
346 // Stage 8: Create analysis facets
347 {
348 $facet: {
349 // Top users by engagement
350 topUsers: [
351 { $sort: { engagementScore: -1 } },
352 { $limit: 20 },
353 {
354 $project: {
355 firstName: 1,
356 lastName: 1,
357 email: 1,
358 postCount: 1,
359 totalViews: 1,
360 engagementScore: 1,
361 userSegment: 1,
362 createdAt: 1
363 }
364 }
365 ],
366
367 // User segments distribution
368 segmentDistribution: [
369 {
370 $group: {
371 _id: '$userSegment',
372 count: { $sum: 1 },
373 avgEngagement: { $avg: '$engagementScore' },
374 avgPosts: { $avg: '$postCount' }
375 }
376 }
377 ],
378
379 // Monthly user activity
380 monthlyActivity: [
381 {
382 $group: {
383 _id: {
384 year: { $year: '$createdAt' },
385 month: { $month: '$createdAt' }
386 },
387 newUsers: { $sum: 1 },
388 avgEngagement: { $avg: '$engagementScore' }
389 }
390 },
391 { $sort: { '_id.year': -1, '_id.month': -1 } },
392 { $limit: 12 }
393 ],
394
395 // Overall engagement stats
396 overallStats: [
397 {
398 $group: {
399 _id: null,
400 totalUsers: { $sum: 1 },
401 totalPosts: { $sum: '$postCount' },
402 avgEngagementScore: { $avg: '$engagementScore' },
403 avgPostsPerUser: { $avg: '$postCount' },
404 maxEngagement: { $max: '$engagementScore' },
405 minEngagement: { $min: '$engagementScore' }
406 }
407 }
408 ]
409 }
410 }
411 ]);
412
413 return {
414 success: true,
415 engagement: engagement[0],
416 generatedAt: new Date()
417 };
418
419 } catch (error) {
420 throw new Error(\`User engagement analytics failed: \${error.message}\`);
421 }
422 }
423
424 // Content performance by tag
425 async getTagPerformanceAnalytics() {
426 try {
427 const tagAnalytics = await Tag.aggregate([
428 // Stage 1: Lookup posts for each tag
429 {
430 $lookup: {
431 from: 'posts',
432 localField: '_id',
433 foreignField: 'tags',
434 as: 'posts',
435 pipeline: [
436 { $match: { status: 'published' } }
437 ]
438 }
439 },
440
441 // Stage 2: Calculate tag metrics
442 {
443 $addFields: {
444 postCount: { $size: '$posts' },
445 totalViews: { $sum: '$posts.views' },
446 totalLikes: { $sum: { $size: '$posts.likes' } },
447 totalComments: { $sum: { $size: '$posts.comments' } },
448 avgViews: { $avg: '$posts.views' },
449 avgLikes: { $avg: { $size: '$posts.likes' } },
450 avgComments: { $avg: { $size: '$posts.comments' } }
451 }
452 },
453
454 // Stage 3: Calculate engagement score
455 {
456 $addFields: {
457 engagementScore: {
458 $add: [
459 { $multiply: ['$totalLikes', 1] },
460 { $multiply: ['$totalComments', 2] },
461 { $multiply: ['$totalViews', 0.1] }
462 ]
463 }
464 }
465 },
466
467 // Stage 4: Filter tags with posts
468 {
469 $match: { postCount: { $gt: 0 } }
470 },
471
472 // Stage 5: Sort by engagement
473 { $sort: { engagementScore: -1 } },
474
475 // Stage 6: Project final results
476 {
477 $project: {
478 name: 1,
479 description: 1,
480 color: 1,
481 postCount: 1,
482 totalViews: 1,
483 totalLikes: 1,
484 totalComments: 1,
485 avgViews: { $round: ['$avgViews', 2] },
486 avgLikes: { $round: ['$avgLikes', 2] },
487 avgComments: { $round: ['$avgComments', 2] },
488 engagementScore: { $round: ['$engagementScore', 2] },
489 createdAt: 1
490 }
491 }
492 ]);
493
494 return {
495 success: true,
496 tagAnalytics,
497 generatedAt: new Date()
498 };
499
500 } catch (error) {
501 throw new Error(\`Tag performance analytics failed: \${error.message}\`);
502 }
503 }
504}
505
506module.exports = new AnalyticsService();Learn about Practice Exercises.
Exercise 1: E-commerce Product Catalog
Create a complete product catalog system with categories, variants, inventory tracking, and review aggregation using MongoDB and Mongoose.
Exercise 2: User Profile Management
Build a comprehensive user profile system with address management, preferences, social connections, and activity tracking.
Exercise 3: Blog Analytics Dashboard
Implement advanced analytics using aggregation pipelines for post performance, user engagement, and content optimization insights.
Schema Design:
Use Mongoose for robust schema validation and type safety
Error Handling:
Implement comprehensive error handling for database operations
Performance:
Optimize queries with proper indexing and population strategies
Relationships:
Master references and embedding patterns for data modeling
Aggregation:
Leverage aggregation pipelines for complex data analysis
Production Ready:
Include proper connection management and monitoring
You've mastered MongoDB fundamentals with Node.js! You can now design efficient schemas, implement robust CRUD operations, and leverage powerful aggregation pipelines for data analysis.
Your progress is saved in this browser, on this device only.