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
_idfield 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
mongodumpor Atlas backups - Monitoring: Use
db.currentOp(),db.stats(), and MongoDB Atlas monitoring to track performance - Version Your Schema: Use a
schema_versionfield 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