Skip to main content

MongoDB for JavaScript Developers

Published: November 25, 2025 Updated: May 24, 2026 Larry Qu 9 min read

MongoDB is a popular NoSQL database that stores data in flexible, JSON-like documents. For JavaScript developers, especially those working with Node.js, MongoDB feels natural due to its document-oriented structure that aligns well with JavaScript objects. This post covers everything from basic setup to advanced patterns for using MongoDB in JavaScript applications.

Why MongoDB for JavaScript Developers?

  • JSON-like Documents: MongoDB uses BSON (Binary JSON), which maps directly to JavaScript objects, making data manipulation intuitive
  • Schemaless Design: No rigid schemas mean faster development and easier iteration
  • Scalability: Horizontal scaling with sharding suits modern web apps
  • Rich Ecosystem: Native drivers for Node.js, plus ODMs like Mongoose for added structure
  • Developer Velocity: Change document structure without migrations during development

Getting Started

Installation and Setup

Install MongoDB locally or use a cloud service like MongoDB Atlas. For Node.js, install the official driver:

npm install mongodb

For Mongoose (recommended for most applications):

npm install mongoose

Connecting to MongoDB with the Native Driver

const { MongoClient } = require("mongodb");

const uri = "mongodb://localhost:27017";
const client = new MongoClient(uri, {
  maxPoolSize: 10,
  serverSelectionTimeoutMS: 5000,
  socketTimeoutMS: 45000,
});

async function connect() {
  try {
    await client.connect();
    console.log("Connected to MongoDB");
    const db = client.db("mydatabase");
    return db;
  } catch (error) {
    console.error("Connection failed:", error.message);
    process.exit(1);
  }
}

// Reuse the client — do not create a new connection for every request
module.exports = { client, connect };

Connecting with Mongoose

const mongoose = require("mongoose");

const MONGODB_URI = process.env.MONGODB_URI || "mongodb://localhost:27017/mydb";

mongoose
  .connect(MONGODB_URI, {
    maxPoolSize: 10,
  })
  .then(() => console.log("Mongoose connected"))
  .catch((err) => console.error("Mongoose connection error:", err));

// Connection events
mongoose.connection.on("disconnected", () => {
  console.log("Mongoose disconnected");
});

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

Connection Pooling Best Practices

The MongoDB driver maintains a connection pool by default. Reuse the same MongoClient instance across your application instead of creating new connections.

// Good — singleton pattern
class Database {
  static instance;

  static async getInstance() {
    if (!Database.instance) {
      const client = new MongoClient(process.env.MONGODB_URI, {
        maxPoolSize: 20,
        minPoolSize: 5,
      });
      await client.connect();
      Database.instance = client.db();
    }
    return Database.instance;
  }
}

Schema Design with Mongoose

Mongoose provides schema validation, middleware, and a richer API on top of the MongoDB driver.

const mongoose = require("mongoose");
const { Schema } = mongoose;

// User schema
const userSchema = new Schema(
  {
    name: {
      first: { type: String, required: true, trim: true },
      last: { type: String, required: true, trim: true },
    },
    email: {
      type: String,
      required: true,
      unique: true,
      lowercase: true,
      match: /^\S+@\S+\.\S+$/,
    },
    age: { type: Number, min: 13, max: 120 },
    roles: { type: [String], default: ["user"], enum: ["user", "admin", "moderator"] },
    address: {
      street: String,
      city: String,
      state: { type: String, maxlength: 2 },
      zip: String,
    },
    metadata: { type: Schema.Types.Mixed },
  },
  {
    timestamps: true, // adds createdAt and updatedAt
    toJSON: { virtuals: true },
  },
);

// Virtual property — computed field not stored in MongoDB
userSchema.virtual("fullName").get(function () {
  return `${this.name.first} ${this.name.last}`;
});

// Pre-save middleware — hash password before saving
userSchema.pre("save", async function (next) {
  if (this.isModified("password")) {
    this.passwordHash = await bcrypt.hash(this.password, 12);
    delete this.password;
  }
  next();
});

// Instance method
userSchema.methods.isAdmin = function () {
  return this.roles.includes("admin");
};

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

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

Advanced Schema Design for Real Applications

// Product schema with embedded reviews and inventory
const productSchema = new Schema(
  {
    name: { type: String, required: true, index: true },
    slug: { type: String, required: true, unique: true },
    description: { type: String, maxlength: 2000 },
    price: { type: Number, required: true, min: 0 },
    category: { type: Schema.Types.ObjectId, ref: "Category", index: true },
    tags: [String],
    variants: [
      {
        sku: { type: String, required: true },
        color: String,
        size: String,
        stock: { type: Number, default: 0, min: 0 },
        price: Number,
      },
    ],
    ratings: {
      average: { type: Number, default: 0 },
      count: { type: Number, default: 0 },
    },
    isActive: { type: Boolean, default: true },
  },
  { timestamps: true },
);

productSchema.index({ price: 1, "ratings.average": -1 });
productSchema.index({ tags: 1 });
productSchema.index({ name: "text", description: "text" });

CRUD Operations with Async/Await

Create

const { MongoClient } = require("mongodb");

async function createUser(db, userData) {
  const users = db.collection("users");

  try {
    const result = await users.insertOne({
      ...userData,
      createdAt: new Date(),
      updatedAt: new Date(),
    });
    return { success: true, id: result.insertedId };
  } catch (err) {
    if (err.code === 11000) {
      return { success: false, error: "Duplicate key — user already exists" };
    }
    throw err;
  }
}

async function createBatchUsers(db, users) {
  const result = await db.collection("users").insertMany(users, {
    ordered: false,
  });
  console.log(`Inserted ${result.insertedCount} users`);
  return result;
}

// Mongoose version
async function createProduct(data) {
  try {
    const product = new Product(data);
    const saved = await product.save();
    return saved;
  } catch (err) {
    if (err.name === "ValidationError") {
      const messages = Object.values(err.errors).map((e) => e.message);
      throw new Error(`Validation failed: ${messages.join(", ")}`);
    }
    throw err;
  }
}

Read

async function findUsers(db, filters) {
  const query = {};

  if (filters.minAge) query.age = { $gte: filters.minAge };
  if (filters.role) query.roles = filters.role;
  if (filters.search) {
    query.$text = { $search: filters.search };
  }

  const options = {
    projection: { passwordHash: 0, __v: 0 },
    sort: { createdAt: -1 },
    limit: Math.min(filters.limit || 20, 100),
    skip: (filters.page || 0) * (filters.limit || 20),
  };

  const [users, total] = await Promise.all([
    db.collection("users").find(query, options).toArray(),
    db.collection("users").countDocuments(query),
  ]);

  return { users, total, page: filters.page || 0, totalPages: Math.ceil(total / options.limit) };
}

// Mongoose version with chaining
async function findProducts(filters) {
  const query = Product.find({ isActive: true });

  if (filters.category) query.where("category").equals(filters.category);
  if (filters.minPrice) query.where("price").gte(filters.minPrice);
  if (filters.maxPrice) query.where("price").lte(filters.maxPrice);
  if (filters.tags) query.where("tags").in(filters.tags);

  const [products, total] = await Promise.all([
    query.sort({ "ratings.average": -1 }).limit(20).skip(0).lean(),
    Product.countDocuments(query.getFilter()),
  ]);

  return { products, total };
}

Update

async function updateUserProfile(db, userId, updates) {
  const allowedFields = ["name", "email", "age", "address"];
  const sanitized = {};

  for (const key of Object.keys(updates)) {
    if (allowedFields.includes(key)) {
      sanitized[key] = updates[key];
    }
  }

  sanitized.updatedAt = new Date();

  const result = await db.collection("users").updateOne(
    { _id: userId },
    { $set: sanitized },
  );

  if (result.matchedCount === 0) {
    throw new Error("User not found");
  }

  return { modified: result.modifiedCount > 0 };
}

async function bulkUpdateProducts(db, category, discountPercent) {
  const result = await db.collection("products").updateMany(
    { category, isActive: true },
    [
      {
        $set: {
          price: { $multiply: ["$price", 1 - discountPercent / 100] },
          updatedAt: new Date(),
          onSale: true,
        },
      },
    ],
  );

  return { matched: result.matchedCount, modified: result.modifiedCount };
}

// Mongoose findOneAndUpdate
async function updateOrderStatus(orderId, status) {
  const order = await Order.findByIdAndUpdate(
    orderId,
    { status, updatedAt: new Date() },
    { new: true, runValidators: true },
  );

  if (!order) {
    throw new NotFoundError("Order not found");
  }

  return order;
}

Delete

async function deleteUser(db, userId) {
  const session = db.client.startSession();

  try {
    session.startTransaction();

    // Delete user data across collections
    await db.collection("users").deleteOne({ _id: userId }, { session });
    await db.collection("sessions").deleteMany({ userId }, { session });
    await db.collection("comments").deleteMany({ userId }, { session });

    await session.commitTransaction();
    return { deleted: true };
  } catch (err) {
    await session.abortTransaction();
    throw err;
  } finally {
    session.endSession();
  }
}

// Soft delete pattern
async function softDeleteProduct(productId) {
  const result = await Product.findByIdAndUpdate(
    productId,
    { isActive: false, deletedAt: new Date() },
    { new: true },
  );
  return result;
}

Indexing for Performance

Indexes are critical for query performance. Without proper indexes, MongoDB performs collection scans.

Index Types

// Single field index
await db.collection("users").createIndex({ email: 1 }, { unique: true });

// Compound index — supports queries on email alone or email + status
await db.collection("users").createIndex({ email: 1, status: 1 });

// Multikey index — for array fields
await db.collection("products").createIndex({ tags: 1 });

// Text index — for full-text search
await db.collection("articles").createIndex(
  { title: "text", body: "text" },
  { weights: { title: 10, body: 1 } },
);

// Geospatial index
await db.collection("locations").createIndex({ coordinates: "2dsphere" });

// TTL index — auto-expire documents
await db.collection("sessions").createIndex(
  { createdAt: 1 },
  { expireAfterSeconds: 86400 },
);

Index Strategy

// Analyze query patterns and create indexes accordingly
async function analyzeAndIndex() {
  // Check existing indexes
  const indexes = await db.collection("orders").indexes();
  console.log("Current indexes:", indexes.length);

  // Use explain to verify index usage
  const explanation = await db.collection("orders").find({
    customerId: "cust123",
    status: "pending",
  }).sort({ createdAt: -1 }).explain("executionStats");

  console.log("Winning plan:", explanation.queryPlanner.winningPlan);
  console.log("Examined docs:", explanation.executionStats.totalDocsExamined);
}

// Index for the most common query pattern
await db.collection("orders").createIndex(
  { customerId: 1, status: 1, createdAt: -1 },
);

Aggregation Pipeline

The aggregation pipeline processes documents through a series of stages.

Basic Aggregation

const pipeline = [
  { $match: { age: { $gte: 18 } } },
  { $group: { _id: "$status", count: { $sum: 1 }, avgAge: { $avg: "$age" } } },
  { $sort: { count: -1 } },
];

const results = await collection.aggregate(pipeline).toArray();

Real-World Aggregation Example

// E-commerce customer value analysis
const topCustomers = await db.collection("orders").aggregate([
  { $match: { createdAt: { $gte: thirtyDaysAgo } } },
  { $unwind: "$items" },
  {
    $group: {
      _id: "$customerId",
      totalSpent: { $sum: { $multiply: ["$items.price", "$items.qty"] } },
      orderCount: { $sum: 1 },
    },
  },
  {
    $project: {
      _id: 0,
      customerId: "$_id",
      totalSpent: { $round: ["$totalSpent", 2] },
      orderCount: 1,
      avgOrderValue: { $round: [{ $divide: ["$totalSpent", "$orderCount"] }, 2] },
    },
  },
  { $sort: { totalSpent: -1 } },
  { $limit: 10 },
]).toArray();

Transactions

MongoDB supports multi-document ACID transactions since version 4.0.

async function transferFunds(fromAccountId, toAccountId, amount) {
  const session = client.startSession();

  try {
    session.startTransaction({
      readConcern: { level: "snapshot" },
      writeConcern: { w: "majority" },
    });

    const fromAccount = await db.collection("accounts").findOne(
      { _id: fromAccountId },
      { session },
    );

    if (!fromAccount || fromAccount.balance < amount) {
      throw new Error("Insufficient funds");
    }

    await db.collection("accounts").updateOne(
      { _id: fromAccountId },
      { $inc: { balance: -amount } },
      { session },
    );

    await db.collection("accounts").updateOne(
      { _id: toAccountId },
      { $inc: { balance: amount } },
      { session },
    );

    await db.collection("transactions").insertOne(
      {
        from: fromAccountId,
        to: toAccountId,
        amount,
        type: "transfer",
        createdAt: new Date(),
      },
      { session },
    );

    await session.commitTransaction();
    return { success: true };
  } catch (err) {
    await session.abortTransaction();
    throw err;
  } finally {
    session.endSession();
  }
}

Comparison to SQL

Aspect MongoDB (NoSQL) SQL Databases
Structure Flexible documents Rigid tables
Queries JSON-like API SQL statements
Scaling Horizontal (sharding) Vertical (more powerful HW)
Joins Embedded docs or $lookup Native JOINs
Transactions Multi-document (since 4.0) ACID from inception
Schema Dynamic, per-document Fixed, migrations required
Development speed Fast iteration Schema-first approach

Best Practices

  • Use ObjectId for IDs: MongoDB’s default _id field is an ObjectId — let MongoDB generate it
  • Handle Errors: Always wrap operations in try-catch and use specific error handling for duplicate keys, validation errors, and network issues
  • Connection Pooling: The driver handles pooling; reuse the client across your application
  • Validation: Use Mongoose for schema validation in production applications
  • Index Strategically: Create indexes that match your query patterns; use explain() to verify
  • Projections: Always limit returned fields with projection to reduce network overhead
  • Security: Use authentication, connection encryption (TLS), and avoid exposing connection strings in client-side code
  • Backup: Regularly backup your data using mongodump or Atlas backups
  • Monitoring: Use db.currentOp(), db.stats(), and MongoDB Atlas monitoring to track performance
  • Version Your Schema: Use a schema_version field to handle schema evolution without downtime

Conclusion

MongoDB’s document model makes it a great fit for JavaScript developers building flexible, scalable applications. Start with the basics, experiment with CRUD operations, and leverage aggregation for complex queries. For production apps, consider using Mongoose for additional features like validation and middleware. Always design your schema around your application’s query patterns, and use indexes strategically to maintain performance as your data grows.

For more, check the MongoDB documentation and Node.js driver docs.

Comments

👍 Was this article helpful?