Back to articles
Backend

MongoDB Integration with Node.js: Best Practices

Learn how to effectively integrate MongoDB with Node.js applications. Covers schema design, aggregation pipelines, indexing strategies, and performance optimization.

13 mins read
MongoDBNode.jsDatabaseBackend

MongoDB integration with Node.js applications requires understanding of schema design, query optimization, and best practices for scalable data management. This guide covers everything from basic CRUD operations to advanced aggregation pipelines.

Setting Up MongoDB Connection

JavaScriptconfig/database.js
const mongoose = require('mongoose');const connectDB = async () => {  try {    const conn = await mongoose.connect(process.env.MONGODB_URI, {      useNewUrlParser: true,      useUnifiedTopology: true,      maxPoolSize: 10, // Maintain up to 10 socket connections      serverSelectionTimeoutMS: 5000, // Keep trying to send operations for 5 seconds      socketTimeoutMS: 45000, // Close sockets after 45 seconds of inactivity      bufferCommands: false, // Disable mongoose buffering      bufferMaxEntries: 0 // Disable mongoose buffering    });    console.log(`MongoDB Connected: ${conn.connection.host}`);  } catch (error) {    console.error('Database connection error:', error);    process.exit(1);  }};module.exports = connectDB;

Schema Design Best Practices

JavaScriptmodels/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,    maxlength: [50, 'Name cannot exceed 50 characters']  },  email: {    type: String,    required: [true, 'Email is required'],    unique: true,    lowercase: 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'],    select: false // Don't include password in queries by default  },  role: {    type: String,    enum: ['user', 'admin', 'moderator'],    default: 'user'  },  profile: {    avatar: String,    bio: {      type: String,      maxlength: [500, 'Bio cannot exceed 500 characters']    },    socialLinks: {      twitter: String,      linkedin: String,      github: String    }  },  preferences: {    notifications: {      email: { type: Boolean, default: true },      push: { type: Boolean, default: false }    },    theme: {      type: String,      enum: ['light', 'dark', 'auto'],      default: 'auto'    }  },  lastLogin: Date,  isEmailVerified: {    type: Boolean,    default: false  },  emailVerificationToken: String,  passwordResetToken: String,  passwordResetExpires: Date}, {  timestamps: true,  toJSON: { virtuals: true },  toObject: { virtuals: true }});// Indexes for performanceuserSchema.index({ email: 1 });userSchema.index({ createdAt: -1 });userSchema.index({ 'profile.username': 1 });// Virtual for user's full profile URLuserSchema.virtual('profileUrl').get(function() {  return `/users/${this._id}`;});// Pre-save middleware to hash passworduserSchema.pre('save', async function(next) {  if (!this.isModified('password')) return next();    const salt = await bcrypt.genSalt(12);  this.password = await bcrypt.hash(this.password, salt);  next();});// Instance method to compare passwordsuserSchema.methods.comparePassword = async function(candidatePassword) {  return await bcrypt.compare(candidatePassword, this.password);};module.exports = mongoose.model('User', userSchema);

Advanced Querying Techniques

JavaScriptservices/userService.js
const User = require('../models/User');class UserService {  // Basic CRUD operations  async createUser(userData) {    try {      const user = new User(userData);      await user.save();      return user;    } catch (error) {      throw new Error(`Failed to create user: ${error.message}`);    }  }  async getUserById(id) {    try {      return await User.findById(id)        .select('-password')        .populate('profile.avatar');    } catch (error) {      throw new Error(`Failed to get user: ${error.message}`);    }  }  // Advanced querying with pagination and filtering  async getUsers(options = {}) {    const {      page = 1,      limit = 10,      sortBy = 'createdAt',      sortOrder = 'desc',      filter = {},      search    } = options;    const query = { ...filter };    // Add search functionality    if (search) {      query.$or = [        { name: { $regex: search, $options: 'i' } },        { email: { $regex: search, $options: 'i' } },        { 'profile.bio': { $regex: search, $options: 'i' } }      ];    }    const skip = (page - 1) * limit;    const sort = { [sortBy]: sortOrder === 'desc' ? -1 : 1 };    try {      const [users, total] = await Promise.all([        User.find(query)          .select('-password')          .sort(sort)          .skip(skip)          .limit(limit)          .lean(), // Use lean() for better performance when you don't need Mongoose documents        User.countDocuments(query)      ]);      return {        users,        pagination: {          page,          limit,          total,          pages: Math.ceil(total / limit)        }      };    } catch (error) {      throw new Error(`Failed to get users: ${error.message}`);    }  }  // Bulk operations  async updateManyUsers(filter, update) {    try {      const result = await User.updateMany(filter, update);      return result;    } catch (error) {      throw new Error(`Failed to update users: ${error.message}`);    }  }}module.exports = new UserService();

Aggregation Pipelines

JavaScriptservices/analyticsService.js
const User = require('../models/User');const Post = require('../models/Post');class AnalyticsService {  // User registration analytics  async getUserRegistrationStats(startDate, endDate) {    try {      const stats = await User.aggregate([        {          $match: {            createdAt: {              $gte: new Date(startDate),              $lte: new Date(endDate)            }          }        },        {          $group: {            _id: {              year: { $year: '$createdAt' },              month: { $month: '$createdAt' },              day: { $dayOfMonth: '$createdAt' }            },            count: { $sum: 1 },            users: { $push: '$email' }          }        },        {          $sort: { '_id.year': 1, '_id.month': 1, '_id.day': 1 }        },        {          $project: {            date: {              $dateFromParts: {                year: '$_id.year',                month: '$_id.month',                day: '$_id.day'              }            },            count: 1,            _id: 0          }        }      ]);      return stats;    } catch (error) {      throw new Error(`Failed to get registration stats: ${error.message}`);    }  }  // User engagement analytics  async getUserEngagementStats() {    try {      const stats = await User.aggregate([        {          $lookup: {            from: 'posts',            localField: '_id',            foreignField: 'author',            as: 'posts'          }        },        {          $lookup: {            from: 'comments',            localField: '_id',            foreignField: 'author',            as: 'comments'          }        },        {          $addFields: {            postCount: { $size: '$posts' },            commentCount: { $size: '$comments' },            engagementScore: {              $add: [                { $multiply: [{ $size: '$posts' }, 2] },                { $size: '$comments' }              ]            }          }        },        {          $group: {            _id: null,            totalUsers: { $sum: 1 },            activeUsers: {              $sum: {                $cond: [{ $gt: ['$engagementScore', 0] }, 1, 0]              }            },            avgPostsPerUser: { $avg: '$postCount' },            avgCommentsPerUser: { $avg: '$commentCount' },            topUsers: {              $push: {                $cond: [                  { $gt: ['$engagementScore', 10] },                  {                    name: '$name',                    email: '$email',                    engagementScore: '$engagementScore'                  },                  null                ]              }            }          }        },        {          $project: {            _id: 0,            totalUsers: 1,            activeUsers: 1,            engagementRate: {              $multiply: [                { $divide: ['$activeUsers', '$totalUsers'] },                100              ]            },            avgPostsPerUser: { $round: ['$avgPostsPerUser', 2] },            avgCommentsPerUser: { $round: ['$avgCommentsPerUser', 2] },            topUsers: {              $filter: {                input: '$topUsers',                cond: { $ne: ['$$this', null] }              }            }          }        }      ]);      return stats[0] || {};    } catch (error) {      throw new Error(`Failed to get engagement stats: ${error.message}`);    }  }}module.exports = new AnalyticsService();

Error Handling and Transactions

JavaScriptservices/orderService.js
const mongoose = require('mongoose');const User = require('../models/User');const Order = require('../models/Order');const Product = require('../models/Product');class OrderService {  async createOrder(userId, orderData) {    const session = await mongoose.startSession();        try {      return await session.withTransaction(async () => {        // Validate user exists        const user = await User.findById(userId).session(session);        if (!user) {          throw new Error('User not found');        }        // Validate and reserve products        const productUpdates = [];        let totalAmount = 0;        for (const item of orderData.items) {          const product = await Product.findById(item.productId).session(session);                    if (!product) {            throw new Error(`Product ${item.productId} not found`);          }                    if (product.stock < item.quantity) {            throw new Error(`Insufficient stock for ${product.name}`);          }          // Reserve stock          productUpdates.push({            updateOne: {              filter: { _id: item.productId },              update: { $inc: { stock: -item.quantity } }            }          });          totalAmount += product.price * item.quantity;        }        // Update product stock        await Product.bulkWrite(productUpdates, { session });        // Create order        const order = new Order({          user: userId,          items: orderData.items,          totalAmount,          shippingAddress: orderData.shippingAddress,          status: 'pending'        });        await order.save({ session });        // Update user's order history        await User.findByIdAndUpdate(          userId,          { $push: { orderHistory: order._id } },          { session }        );        return order;      });    } catch (error) {      throw new Error(`Failed to create order: ${error.message}`);    } finally {      await session.endSession();    }  }}module.exports = new OrderService();

Performance Optimization

  • Use indexes strategically - compound indexes for multiple field queries
  • Implement connection pooling with appropriate pool size
  • Use lean() queries when you don't need Mongoose document features
  • Implement proper pagination to avoid loading large datasets
  • Use projection to select only needed fields
  • Cache frequently accessed data with Redis
  • Monitor slow queries with MongoDB Profiler
  • Use aggregation pipelines instead of multiple queries when possible
JavaScriptmiddleware/cache.js
const redis = require('redis');const client = redis.createClient();const cache = (duration = 300) => {  return async (req, res, next) => {    const key = `cache:${req.originalUrl}`;        try {      const cached = await client.get(key);            if (cached) {        return res.json(JSON.parse(cached));      }            // Store original json method      const originalJson = res.json;            // Override json method to cache response      res.json = function(data) {        client.setex(key, duration, JSON.stringify(data));        originalJson.call(this, data);      };            next();    } catch (error) {      console.error('Cache error:', error);      next();    }  };};module.exports = cache;
Schema Design

Flexible document structure with validation, indexing, and relationships.

Aggregation Pipelines

Powerful data processing and transformation for complex queries.

Performance Optimization

Indexing strategies, connection pooling, and query optimization.

Transactions & Reliability

ACID transactions, error handling, and data consistency.

MongoDB's flexibility is both its strength and potential weakness. Proper schema design and query optimization are crucial for building scalable applications.