- Published on
MongoDB with Node.js: Complete Integration Guide
- Authors
- Name
- Mamun Rashid
- @mmncit
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:
- MongoDB Native Driver: Direct, low-level access to MongoDB
- 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
- Previous: MongoDB Aggregation Pipeline
- Next: MongoDB Production Deployment
- Hub: MongoDB Zero to Hero - Complete Guide
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