Published on

MongoDB with Node.js: Complete Integration Guide

Authors

MongoDB with Node.js: Complete Integration Guide

Welcome to Part 7 of our MongoDB Zero to Hero series. After mastering the Aggregation Pipeline, it's time to integrate MongoDB with Node.js applications to build real-world applications.

Introduction to MongoDB Drivers

There are two main ways to work with MongoDB in Node.js:

  1. MongoDB Native Driver: Direct, low-level access to MongoDB
  2. Mongoose ODM: Object Document Mapper with schema validation and additional features

We'll explore both approaches with practical examples.

Setting Up the Environment

Project Initialization

# Create new project
mkdir mongodb-nodejs-app
cd mongodb-nodejs-app

# Initialize npm project
npm init -y

# Install dependencies
npm install mongodb mongoose express dotenv

# Install development dependencies
npm install --save-dev nodemon

Project Structure

mongodb-nodejs-app/
├── config/
│   └── database.js
├── models/
│   ├── User.js
│   └── Product.js
├── routes/
│   ├── users.js
│   └── products.js
├── middleware/
│   └── auth.js
├── .env
├── app.js
└── server.js

Environment Configuration

// .env
MONGODB_URI=mongodb://localhost:27017/myapp
MONGODB_URI_ATLAS=mongodb+srv://username:password@cluster.mongodb.net/myapp
PORT=3000
NODE_ENV=development

MongoDB Native Driver

Basic Connection

// config/database.js
const { MongoClient } = require('mongodb');
require('dotenv').config();

class Database {
    constructor() {
        this.client = null;
        this.db = null;
    }

    async connect() {
        try {
            this.client = new MongoClient(process.env.MONGODB_URI, {
                useUnifiedTopology: true,
                maxPoolSize: 10, // Maximum connection pool size
                serverSelectionTimeoutMS: 5000, // Timeout for server selection
                socketTimeoutMS: 45000, // Socket timeout
                bufferMaxEntries: 0, // Disable mongoose buffering
                bufferCommands: false, // Disable mongoose buffering
            });

            await this.client.connect();
            this.db = this.client.db();

            console.log('Connected to MongoDB with Native Driver');

            // Test the connection
            await this.db.admin().ping();
            console.log('MongoDB ping successful');
        } catch (error) {
            console.error('MongoDB connection error:', error);
            process.exit(1);
        }
    }

    async disconnect() {
        if (this.client) {
            await this.client.close();
            console.log('Disconnected from MongoDB');
        }
    }

    getDb() {
        if (!this.db) {
            throw new Error('Database not connected');
        }
        return this.db;
    }

    getCollection(collectionName) {
        return this.getDb().collection(collectionName);
    }
}

module.exports = new Database();

CRUD Operations with Native Driver

// services/UserService.js
const { ObjectId } = require('mongodb');
const database = require('../config/database');

class UserService {
    constructor() {
        this.collection = 'users';
    }

    getCollection() {
        return database.getCollection(this.collection);
    }

    // Create user
    async createUser(userData) {
        try {
            const user = {
                ...userData,
                createdAt: new Date(),
                updatedAt: new Date(),
            };

            const result = await this.getCollection().insertOne(user);

            return {
                success: true,
                userId: result.insertedId,
                user: { _id: result.insertedId, ...user },
            };
        } catch (error) {
            console.error('Error creating user:', error);
            throw error;
        }
    }

    // Find user by ID
    async findUserById(userId) {
        try {
            const user = await this.getCollection().findOne({
                _id: new ObjectId(userId),
            });

            return user;
        } catch (error) {
            console.error('Error finding user:', error);
            throw error;
        }
    }

    // Find user by email
    async findUserByEmail(email) {
        try {
            const user = await this.getCollection().findOne({ email });
            return user;
        } catch (error) {
            console.error('Error finding user by email:', error);
            throw error;
        }
    }

    // Update user
    async updateUser(userId, updateData) {
        try {
            const result = await this.getCollection().updateOne(
                { _id: new ObjectId(userId) },
                {
                    $set: {
                        ...updateData,
                        updatedAt: new Date(),
                    },
                },
            );

            if (result.matchedCount === 0) {
                return { success: false, message: 'User not found' };
            }

            const updatedUser = await this.findUserById(userId);
            return { success: true, user: updatedUser };
        } catch (error) {
            console.error('Error updating user:', error);
            throw error;
        }
    }

    // Delete user
    async deleteUser(userId) {
        try {
            const result = await this.getCollection().deleteOne({
                _id: new ObjectId(userId),
            });

            return {
                success: result.deletedCount > 0,
                deletedCount: result.deletedCount,
            };
        } catch (error) {
            console.error('Error deleting user:', error);
            throw error;
        }
    }

    // Find users with filters and pagination
    async findUsers(filters = {}, options = {}) {
        try {
            const { page = 1, limit = 10, sortBy = 'createdAt', sortOrder = -1, search } = options;

            const query = { ...filters };

            // Add search functionality
            if (search) {
                query.$or = [
                    { name: { $regex: search, $options: 'i' } },
                    { email: { $regex: search, $options: 'i' } },
                ];
            }

            const skip = (page - 1) * limit;

            const [users, totalCount] = await Promise.all([
                this.getCollection()
                    .find(query)
                    .sort({ [sortBy]: sortOrder })
                    .skip(skip)
                    .limit(parseInt(limit))
                    .toArray(),
                this.getCollection().countDocuments(query),
            ]);

            return {
                users,
                pagination: {
                    currentPage: parseInt(page),
                    totalPages: Math.ceil(totalCount / limit),
                    totalCount,
                    hasNext: skip + users.length < totalCount,
                    hasPrev: page > 1,
                },
            };
        } catch (error) {
            console.error('Error finding users:', error);
            throw error;
        }
    }

    // Aggregation example: User statistics
    async getUserStatistics() {
        try {
            const stats = await this.getCollection()
                .aggregate([
                    {
                        $group: {
                            _id: null,
                            totalUsers: { $sum: 1 },
                            avgAge: { $avg: '$age' },
                            oldestUser: { $max: '$age' },
                            youngestUser: { $min: '$age' },
                        },
                    },
                    {
                        $project: {
                            _id: 0,
                            totalUsers: 1,
                            avgAge: { $round: ['$avgAge', 1] },
                            oldestUser: 1,
                            youngestUser: 1,
                        },
                    },
                ])
                .toArray();

            return (
                stats[0] || {
                    totalUsers: 0,
                    avgAge: 0,
                    oldestUser: 0,
                    youngestUser: 0,
                }
            );
        } catch (error) {
            console.error('Error getting user statistics:', error);
            throw error;
        }
    }

    // Bulk operations
    async createMultipleUsers(usersData) {
        try {
            const users = usersData.map((user) => ({
                ...user,
                createdAt: new Date(),
                updatedAt: new Date(),
            }));

            const result = await this.getCollection().insertMany(users);

            return {
                success: true,
                insertedCount: result.insertedCount,
                insertedIds: result.insertedIds,
            };
        } catch (error) {
            console.error('Error creating multiple users:', error);
            throw error;
        }
    }

    // Transaction example
    async transferUserData(fromUserId, toUserId, data) {
        const session = database.client.startSession();

        try {
            await session.withTransaction(async () => {
                // Remove data from source user
                await this.getCollection().updateOne(
                    { _id: new ObjectId(fromUserId) },
                    { $unset: data },
                    { session },
                );

                // Add data to destination user
                await this.getCollection().updateOne(
                    { _id: new ObjectId(toUserId) },
                    { $set: { ...data, updatedAt: new Date() } },
                    { session },
                );
            });

            return { success: true, message: 'Transfer completed' };
        } catch (error) {
            console.error('Transaction failed:', error);
            throw error;
        } finally {
            await session.endSession();
        }
    }
}

module.exports = new UserService();

Express Routes with Native Driver

// routes/users.js
const express = require('express');
const { ObjectId } = require('mongodb');
const UserService = require('../services/UserService');

const router = express.Router();

// Create user
router.post('/', async (req, res) => {
    try {
        const { name, email, age } = req.body;

        // Basic validation
        if (!name || !email) {
            return res.status(400).json({
                success: false,
                message: 'Name and email are required',
            });
        }

        // Check if user exists
        const existingUser = await UserService.findUserByEmail(email);
        if (existingUser) {
            return res.status(409).json({
                success: false,
                message: 'User with this email already exists',
            });
        }

        const result = await UserService.createUser({ name, email, age });
        res.status(201).json(result);
    } catch (error) {
        res.status(500).json({
            success: false,
            message: 'Error creating user',
            error: error.message,
        });
    }
});

// Get all users with pagination and search
router.get('/', async (req, res) => {
    try {
        const {
            page = 1,
            limit = 10,
            search,
            sortBy = 'createdAt',
            sortOrder = 'desc',
        } = req.query;

        const options = {
            page: parseInt(page),
            limit: parseInt(limit),
            search,
            sortBy,
            sortOrder: sortOrder === 'desc' ? -1 : 1,
        };

        const result = await UserService.findUsers({}, options);
        res.json({ success: true, data: result });
    } catch (error) {
        res.status(500).json({
            success: false,
            message: 'Error fetching users',
            error: error.message,
        });
    }
});

// Get user by ID
router.get('/:id', async (req, res) => {
    try {
        const { id } = req.params;

        if (!ObjectId.isValid(id)) {
            return res.status(400).json({
                success: false,
                message: 'Invalid user ID format',
            });
        }

        const user = await UserService.findUserById(id);

        if (!user) {
            return res.status(404).json({
                success: false,
                message: 'User not found',
            });
        }

        res.json({ success: true, data: user });
    } catch (error) {
        res.status(500).json({
            success: false,
            message: 'Error fetching user',
            error: error.message,
        });
    }
});

// Update user
router.put('/:id', async (req, res) => {
    try {
        const { id } = req.params;
        const updateData = req.body;

        if (!ObjectId.isValid(id)) {
            return res.status(400).json({
                success: false,
                message: 'Invalid user ID format',
            });
        }

        // Remove sensitive fields that shouldn't be updated directly
        delete updateData._id;
        delete updateData.createdAt;

        const result = await UserService.updateUser(id, updateData);

        if (!result.success) {
            return res.status(404).json(result);
        }

        res.json(result);
    } catch (error) {
        res.status(500).json({
            success: false,
            message: 'Error updating user',
            error: error.message,
        });
    }
});

// Delete user
router.delete('/:id', async (req, res) => {
    try {
        const { id } = req.params;

        if (!ObjectId.isValid(id)) {
            return res.status(400).json({
                success: false,
                message: 'Invalid user ID format',
            });
        }

        const result = await UserService.deleteUser(id);

        if (!result.success) {
            return res.status(404).json({
                success: false,
                message: 'User not found',
            });
        }

        res.json({ success: true, message: 'User deleted successfully' });
    } catch (error) {
        res.status(500).json({
            success: false,
            message: 'Error deleting user',
            error: error.message,
        });
    }
});

// Get user statistics
router.get('/stats/summary', async (req, res) => {
    try {
        const stats = await UserService.getUserStatistics();
        res.json({ success: true, data: stats });
    } catch (error) {
        res.status(500).json({
            success: false,
            message: 'Error fetching user statistics',
            error: error.message,
        });
    }
});

module.exports = router;

Mongoose ODM

Mongoose Connection

// config/mongoose.js
const mongoose = require('mongoose');
require('dotenv').config();

class MongooseConnection {
    constructor() {
        this.isConnected = false;
    }

    async connect() {
        if (this.isConnected) {
            console.log('Already connected to MongoDB via Mongoose');
            return;
        }

        try {
            const options = {
                useUnifiedTopology: true,
                maxPoolSize: 10,
                serverSelectionTimeoutMS: 5000,
                socketTimeoutMS: 45000,
                bufferMaxEntries: 0,
                bufferCommands: false,
            };

            await mongoose.connect(process.env.MONGODB_URI, options);

            this.isConnected = true;
            console.log('Connected to MongoDB via Mongoose');

            // Handle connection events
            mongoose.connection.on('connected', () => {
                console.log('Mongoose connected to MongoDB');
            });

            mongoose.connection.on('error', (err) => {
                console.error('Mongoose connection error:', err);
            });

            mongoose.connection.on('disconnected', () => {
                console.log('Mongoose disconnected from MongoDB');
                this.isConnected = false;
            });

            // Graceful shutdown
            process.on('SIGINT', async () => {
                await this.disconnect();
                process.exit(0);
            });
        } catch (error) {
            console.error('Error connecting to MongoDB via Mongoose:', error);
            process.exit(1);
        }
    }

    async disconnect() {
        if (this.isConnected) {
            await mongoose.connection.close();
            this.isConnected = false;
            console.log('Disconnected from MongoDB via Mongoose');
        }
    }
}

module.exports = new MongooseConnection();

Mongoose Models

// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema(
    {
        name: {
            type: String,
            required: [true, 'Name is required'],
            trim: true,
            minlength: [2, 'Name must be at least 2 characters long'],
            maxlength: [50, 'Name cannot exceed 50 characters'],
        },
        email: {
            type: String,
            required: [true, 'Email is required'],
            unique: true,
            lowercase: true,
            trim: true,
            match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, 'Please enter a valid email'],
        },
        password: {
            type: String,
            required: [true, 'Password is required'],
            minlength: [6, 'Password must be at least 6 characters long'],
            select: false, // Don't include password in queries by default
        },
        age: {
            type: Number,
            min: [0, 'Age cannot be negative'],
            max: [120, 'Age cannot exceed 120'],
        },
        profile: {
            bio: {
                type: String,
                maxlength: [500, 'Bio cannot exceed 500 characters'],
            },
            avatar: {
                type: String,
                default: null,
            },
            preferences: {
                theme: {
                    type: String,
                    enum: ['light', 'dark'],
                    default: 'light',
                },
                notifications: {
                    email: { type: Boolean, default: true },
                    push: { type: Boolean, default: true },
                },
            },
        },
        roles: [
            {
                type: String,
                enum: ['user', 'admin', 'moderator'],
                default: 'user',
            },
        ],
        isActive: {
            type: Boolean,
            default: true,
        },
        lastLogin: {
            type: Date,
            default: null,
        },
        loginAttempts: {
            type: Number,
            default: 0,
        },
        lockUntil: Date,
    },
    {
        timestamps: true, // Adds createdAt and updatedAt
        toJSON: { virtuals: true },
        toObject: { virtuals: true },
    },
);

// Indexes
userSchema.index({ email: 1 });
userSchema.index({ createdAt: -1 });
userSchema.index({ 'profile.preferences.theme': 1 });

// Virtual properties
userSchema.virtual('fullProfile').get(function () {
    return {
        name: this.name,
        email: this.email,
        age: this.age,
        ...this.profile,
    };
});

userSchema.virtual('isLocked').get(function () {
    return !!(this.lockUntil && this.lockUntil > Date.now());
});

// Pre-save middleware
userSchema.pre('save', async function (next) {
    // Hash password if it's modified
    if (!this.isModified('password')) return next();

    try {
        const salt = await bcrypt.genSalt(12);
        this.password = await bcrypt.hash(this.password, salt);
        next();
    } catch (error) {
        next(error);
    }
});

// Instance methods
userSchema.methods.comparePassword = async function (candidatePassword) {
    if (!this.password) return false;
    return bcrypt.compare(candidatePassword, this.password);
};

userSchema.methods.incLoginAttempts = function () {
    // If we have a previous lock that has expired, restart at 1
    if (this.lockUntil && this.lockUntil < Date.now()) {
        return this.updateOne({
            $set: {
                loginAttempts: 1,
            },
            $unset: {
                lockUntil: 1,
            },
        });
    }

    const updates = { $inc: { loginAttempts: 1 } };

    // Lock account after 5 failed attempts for 2 hours
    if (this.loginAttempts + 1 >= 5 && !this.isLocked) {
        updates.$set = {
            lockUntil: Date.now() + 2 * 60 * 60 * 1000, // 2 hours
        };
    }

    return this.updateOne(updates);
};

userSchema.methods.resetLoginAttempts = function () {
    return this.updateOne({
        $unset: {
            loginAttempts: 1,
            lockUntil: 1,
        },
    });
};

userSchema.methods.updateLastLogin = function () {
    return this.updateOne({ $set: { lastLogin: new Date() } });
};

// Static methods
userSchema.statics.findByEmail = function (email) {
    return this.findOne({ email: email.toLowerCase() });
};

userSchema.statics.findActiveUsers = function () {
    return this.find({ isActive: true });
};

userSchema.statics.getUserStats = function () {
    return this.aggregate([
        {
            $group: {
                _id: null,
                totalUsers: { $sum: 1 },
                activeUsers: {
                    $sum: { $cond: [{ $eq: ['$isActive', true] }, 1, 0] },
                },
                avgAge: { $avg: '$age' },
            },
        },
        {
            $project: {
                _id: 0,
                totalUsers: 1,
                activeUsers: 1,
                avgAge: { $round: ['$avgAge', 1] },
            },
        },
    ]);
};

// Query middleware
userSchema.pre(/^find/, function (next) {
    // Exclude locked accounts by default
    if (!this.getQuery().includeLocked) {
        this.find({ $or: [{ lockUntil: { $exists: false } }, { lockUntil: { $lt: Date.now() } }] });
    }
    next();
});

module.exports = mongoose.model('User', userSchema);

Product Model with References

// models/Product.js
const mongoose = require('mongoose');

const reviewSchema = new mongoose.Schema(
    {
        user: {
            type: mongoose.Schema.Types.ObjectId,
            ref: 'User',
            required: true,
        },
        rating: {
            type: Number,
            required: true,
            min: 1,
            max: 5,
        },
        comment: {
            type: String,
            maxlength: [500, 'Review comment cannot exceed 500 characters'],
        },
        helpful: [
            {
                type: mongoose.Schema.Types.ObjectId,
                ref: 'User',
            },
        ],
    },
    {
        timestamps: true,
    },
);

const productSchema = new mongoose.Schema(
    {
        name: {
            type: String,
            required: [true, 'Product name is required'],
            trim: true,
            maxlength: [100, 'Product name cannot exceed 100 characters'],
        },
        description: {
            type: String,
            required: [true, 'Product description is required'],
            maxlength: [2000, 'Description cannot exceed 2000 characters'],
        },
        price: {
            type: Number,
            required: [true, 'Price is required'],
            min: [0, 'Price cannot be negative'],
        },
        category: {
            type: String,
            required: [true, 'Category is required'],
            enum: ['Electronics', 'Clothing', 'Books', 'Home', 'Sports', 'Other'],
        },
        brand: {
            type: String,
            required: true,
            trim: true,
        },
        sku: {
            type: String,
            required: true,
            unique: true,
            uppercase: true,
        },
        inventory: {
            quantity: {
                type: Number,
                required: true,
                min: [0, 'Quantity cannot be negative'],
            },
            reserved: {
                type: Number,
                default: 0,
                min: [0, 'Reserved quantity cannot be negative'],
            },
            warehouse: {
                type: String,
                required: true,
            },
        },
        images: [
            {
                url: {
                    type: String,
                    required: true,
                },
                alt: {
                    type: String,
                    required: true,
                },
                isPrimary: {
                    type: Boolean,
                    default: false,
                },
            },
        ],
        specifications: {
            type: Map,
            of: String,
        },
        tags: [String],
        reviews: [reviewSchema],
        isActive: {
            type: Boolean,
            default: true,
        },
        createdBy: {
            type: mongoose.Schema.Types.ObjectId,
            ref: 'User',
            required: true,
        },
    },
    {
        timestamps: true,
        toJSON: { virtuals: true },
        toObject: { virtuals: true },
    },
);

// Indexes
productSchema.index({ name: 'text', description: 'text' });
productSchema.index({ category: 1, price: 1 });
productSchema.index({ brand: 1 });
productSchema.index({ sku: 1 });
productSchema.index({ tags: 1 });
productSchema.index({ createdAt: -1 });

// Virtuals
productSchema.virtual('averageRating').get(function () {
    if (this.reviews.length === 0) return 0;
    const totalRating = this.reviews.reduce((sum, review) => sum + review.rating, 0);
    return Math.round((totalRating / this.reviews.length) * 10) / 10;
});

productSchema.virtual('reviewCount').get(function () {
    return this.reviews.length;
});

productSchema.virtual('availableQuantity').get(function () {
    return this.inventory.quantity - this.inventory.reserved;
});

productSchema.virtual('isInStock').get(function () {
    return this.availableQuantity > 0;
});

productSchema.virtual('primaryImage').get(function () {
    const primary = this.images.find((img) => img.isPrimary);
    return primary || this.images[0] || null;
});

// Pre-save middleware
productSchema.pre('save', function (next) {
    // Ensure only one primary image
    if (this.isModified('images')) {
        let hasPrimary = false;
        this.images.forEach((img) => {
            if (img.isPrimary && !hasPrimary) {
                hasPrimary = true;
            } else {
                img.isPrimary = false;
            }
        });

        // If no primary image, set first one as primary
        if (!hasPrimary && this.images.length > 0) {
            this.images[0].isPrimary = true;
        }
    }
    next();
});

// Instance methods
productSchema.methods.addReview = function (userId, rating, comment) {
    // Check if user already reviewed
    const existingReview = this.reviews.find(
        (review) => review.user.toString() === userId.toString(),
    );

    if (existingReview) {
        existingReview.rating = rating;
        existingReview.comment = comment;
    } else {
        this.reviews.push({ user: userId, rating, comment });
    }

    return this.save();
};

productSchema.methods.removeReview = function (reviewId) {
    this.reviews.id(reviewId).remove();
    return this.save();
};

productSchema.methods.reserveQuantity = function (quantity) {
    if (this.availableQuantity < quantity) {
        throw new Error('Insufficient inventory');
    }

    this.inventory.reserved += quantity;
    return this.save();
};

productSchema.methods.releaseReservedQuantity = function (quantity) {
    this.inventory.reserved = Math.max(0, this.inventory.reserved - quantity);
    return this.save();
};

// Static methods
productSchema.statics.findByCategory = function (category) {
    return this.find({ category, isActive: true });
};

productSchema.statics.findInPriceRange = function (minPrice, maxPrice) {
    return this.find({
        price: { $gte: minPrice, $lte: maxPrice },
        isActive: true,
    });
};

productSchema.statics.searchProducts = function (searchTerm) {
    return this.find({
        $text: { $search: searchTerm },
        isActive: true,
    }).sort({ score: { $meta: 'textScore' } });
};

productSchema.statics.getTopRated = function (limit = 10) {
    return this.aggregate([
        { $match: { isActive: true } },
        {
            $addFields: {
                avgRating: { $avg: '$reviews.rating' },
                reviewCount: { $size: '$reviews' },
            },
        },
        { $match: { reviewCount: { $gte: 5 } } },
        { $sort: { avgRating: -1, reviewCount: -1 } },
        { $limit: limit },
    ]);
};

module.exports = mongoose.model('Product', productSchema);

Mongoose Service Layer

// services/ProductService.js
const Product = require('../models/Product');
const User = require('../models/User');

class ProductService {
    // Create product
    async createProduct(productData, createdBy) {
        try {
            const product = new Product({
                ...productData,
                createdBy,
            });

            await product.save();
            await product.populate('createdBy', 'name email');

            return { success: true, product };
        } catch (error) {
            if (error.name === 'ValidationError') {
                const errors = Object.values(error.errors).map((err) => err.message);
                return { success: false, message: 'Validation error', errors };
            }

            if (error.code === 11000) {
                return { success: false, message: 'Product with this SKU already exists' };
            }

            throw error;
        }
    }

    // Find products with advanced filtering
    async findProducts(filters = {}, options = {}) {
        try {
            const {
                page = 1,
                limit = 20,
                sortBy = 'createdAt',
                sortOrder = 'desc',
                search,
                category,
                minPrice,
                maxPrice,
                brand,
                tags,
                inStockOnly = false,
            } = options;

            // Build query
            const query = { isActive: true, ...filters };

            if (search) {
                query.$text = { $search: search };
            }

            if (category) {
                query.category = category;
            }

            if (minPrice !== undefined || maxPrice !== undefined) {
                query.price = {};
                if (minPrice !== undefined) query.price.$gte = minPrice;
                if (maxPrice !== undefined) query.price.$lte = maxPrice;
            }

            if (brand) {
                query.brand = new RegExp(brand, 'i');
            }

            if (tags && tags.length > 0) {
                query.tags = { $in: tags };
            }

            if (inStockOnly) {
                query['inventory.quantity'] = { $gt: 0 };
            }

            const skip = (page - 1) * limit;
            const sort = {};
            sort[sortBy] = sortOrder === 'desc' ? -1 : 1;

            // Add text score sorting if search is used
            if (search) {
                sort.score = { $meta: 'textScore' };
            }

            const [products, totalCount] = await Promise.all([
                Product.find(query)
                    .populate('createdBy', 'name email')
                    .populate('reviews.user', 'name')
                    .sort(sort)
                    .skip(skip)
                    .limit(parseInt(limit)),
                Product.countDocuments(query),
            ]);

            return {
                products,
                pagination: {
                    currentPage: parseInt(page),
                    totalPages: Math.ceil(totalCount / limit),
                    totalCount,
                    hasNext: skip + products.length < totalCount,
                    hasPrev: page > 1,
                },
            };
        } catch (error) {
            console.error('Error finding products:', error);
            throw error;
        }
    }

    // Get product by ID with full details
    async getProductById(productId) {
        try {
            const product = await Product.findById(productId)
                .populate('createdBy', 'name email')
                .populate('reviews.user', 'name profile.avatar');

            return product;
        } catch (error) {
            console.error('Error finding product:', error);
            throw error;
        }
    }

    // Update product
    async updateProduct(productId, updateData, userId) {
        try {
            const product = await Product.findById(productId);

            if (!product) {
                return { success: false, message: 'Product not found' };
            }

            // Check if user can update this product
            if (product.createdBy.toString() !== userId.toString()) {
                const user = await User.findById(userId);
                if (!user || !user.roles.includes('admin')) {
                    return { success: false, message: 'Not authorized to update this product' };
                }
            }

            Object.assign(product, updateData);
            await product.save();
            await product.populate('createdBy', 'name email');

            return { success: true, product };
        } catch (error) {
            if (error.name === 'ValidationError') {
                const errors = Object.values(error.errors).map((err) => err.message);
                return { success: false, message: 'Validation error', errors };
            }
            throw error;
        }
    }

    // Add review to product
    async addReview(productId, userId, rating, comment) {
        try {
            const product = await Product.findById(productId);

            if (!product) {
                return { success: false, message: 'Product not found' };
            }

            await product.addReview(userId, rating, comment);
            await product.populate('reviews.user', 'name profile.avatar');

            return { success: true, product };
        } catch (error) {
            console.error('Error adding review:', error);
            throw error;
        }
    }

    // Get product analytics
    async getProductAnalytics(productId) {
        try {
            const analytics = await Product.aggregate([
                { $match: { _id: mongoose.Types.ObjectId(productId) } },
                {
                    $project: {
                        name: 1,
                        category: 1,
                        price: 1,
                        totalReviews: { $size: '$reviews' },
                        averageRating: { $avg: '$reviews.rating' },
                        ratingDistribution: {
                            $reduce: {
                                input: '$reviews',
                                initialValue: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 },
                                in: {
                                    $mergeObjects: [
                                        '$$value',
                                        {
                                            $switch: {
                                                branches: [
                                                    {
                                                        case: { $eq: ['$$this.rating', 1] },
                                                        then: { 1: { $add: ['$$value.1', 1] } },
                                                    },
                                                    {
                                                        case: { $eq: ['$$this.rating', 2] },
                                                        then: { 2: { $add: ['$$value.2', 1] } },
                                                    },
                                                    {
                                                        case: { $eq: ['$$this.rating', 3] },
                                                        then: { 3: { $add: ['$$value.3', 1] } },
                                                    },
                                                    {
                                                        case: { $eq: ['$$this.rating', 4] },
                                                        then: { 4: { $add: ['$$value.4', 1] } },
                                                    },
                                                    {
                                                        case: { $eq: ['$$this.rating', 5] },
                                                        then: { 5: { $add: ['$$value.5', 1] } },
                                                    },
                                                ],
                                                default: '$$value',
                                            },
                                        },
                                    ],
                                },
                            },
                        },
                    },
                },
            ]);

            return analytics[0] || null;
        } catch (error) {
            console.error('Error getting product analytics:', error);
            throw error;
        }
    }
}

module.exports = new ProductService();

What's Next?

You've now learned how to integrate MongoDB with Node.js using both the native driver and Mongoose. Next, explore Production Deployment to learn how to deploy MongoDB applications in production environments.

Series Navigation


This is Part 7 of the MongoDB Zero to Hero series. MongoDB + Node.js is a powerful combination - master these integration patterns to build robust applications.

Enjoyed this post?

Subscribe to get notified about new posts and updates. No spam, unsubscribe anytime.

By subscribing, you agree to our Privacy Policy. You can unsubscribe at any time.

Discussion (0)

This website is still under development. If you encounter any issues, please contact me