MongoDB is the most popular NoSQL database. Instead of tables and rows, it stores data as flexible JSON-like documents in collections. This lesson covers MongoDB's document model, CRUD operations, and schema design patterns.
Documents and Collections
In MongoDB, data is organized into databases, collections, and documents:
Database: myapp
Collection: users
Document: { _id: "...", name: "Alice", email: "a@mail.co" }
Document: { _id: "...", name: "Bob", email: "b@mail.co" }
Collection: posts
Document: { _id: "...", title: "Hello World", author: "Alice" }A document is a JSON object (technically BSON — Binary JSON). Unlike SQL rows, documents in the same collection can have different fields.
Setting Up
Install the MongoDB Node.js driver or Mongoose (an ODM — Object Document Mapper):
npm install mongooseConnect to MongoDB:
const mongoose = require("mongoose");
async function connect() {
await mongoose.connect("mongodb://localhost:27017/myapp");
console.log("Connected to MongoDB");
}
connect();Defining Schemas with Mongoose
Although MongoDB is schemaless, Mongoose lets you define schemas for validation and structure:
const mongoose = require("mongoose");
const userSchema = new mongoose.Schema(
{
name: {
type: String,
required: [true, "Name is required"],
trim: true,
minlength: 2,
maxlength: 100,
},
email: {
type: String,
required: [true, "Email is required"],
unique: true,
lowercase: true,
},
role: {
type: String,
enum: ["user", "admin", "moderator"],
default: "user",
},
age: {
type: Number,
min: 0,
max: 150,
},
tags: [String],
address: {
street: String,
city: String,
country: String,
zip: String,
},
isActive: {
type: Boolean,
default: true,
},
},
{
timestamps: true, // adds createdAt and updatedAt
}
);
const User = mongoose.model("User", userSchema);
module.exports = User;CRUD Operations
Create
// Create one
const user = await User.create({
name: "Alice",
email: "alice@example.com",
tags: ["developer", "designer"],
address: { city: "New York", country: "US" },
});
// Create many
await User.insertMany([
{ name: "Bob", email: "bob@example.com" },
{ name: "Charlie", email: "charlie@example.com" },
]);Read
// Find all
const users = await User.find();
// Find with conditions
const activeUsers = await User.find({ isActive: true });
// Find one
const alice = await User.findOne({ email: "alice@example.com" });
// Find by ID
const user = await User.findById("65a1b2c3d4e5f6a7b8c9d0e1");
// Select specific fields
const names = await User.find().select("name email -_id");
// Sort, limit, skip
const recent = await User.find()
.sort({ createdAt: -1 })
.limit(10)
.skip(0);Query Operators
// Comparison
const expensive = await Product.find({ price: { $gt: 100 } });
const affordable = await Product.find({ price: { $lte: 50 } });
const range = await Product.find({ price: { $gte: 20, $lte: 80 } });
// Logical
const results = await Product.find({
$or: [{ category: "electronics" }, { price: { $lt: 10 } }],
});
// Array operations
const devs = await User.find({ tags: "developer" }); // contains "developer"
const multi = await User.find({ tags: { $all: ["developer", "designer"] } });
// Text search
const matches = await User.find({ name: { $regex: /ali/i } });Operator Reference
| Operator | Meaning | Example |
|---|---|---|
$eq | Equal | { age: { $eq: 25 } } |
$gt | Greater than | { price: { $gt: 100 } } |
$gte | Greater than or equal | { score: { $gte: 70 } } |
$lt | Less than | { stock: { $lt: 5 } } |
$in | In array | { role: { $in: ["admin"] } } |
$or | Logical OR | { $or: [cond1, cond2] } |
$and | Logical AND | { $and: [cond1, cond2] } |
$regex | Pattern match | { name: { $regex: /^A/i } } |
Update
// Update one
await User.findByIdAndUpdate(
userId,
{ name: "Alice Updated" },
{ new: true, runValidators: true }
);
// Update with operators
await User.updateOne(
{ email: "alice@example.com" },
{
$set: { role: "admin" },
$push: { tags: "lead" },
$inc: { loginCount: 1 },
}
);
// Update many
await User.updateMany(
{ isActive: false },
{ $set: { isActive: true } }
);Delete
await User.findByIdAndDelete(userId);
await User.deleteOne({ email: "old@example.com" });
await User.deleteMany({ isActive: false });Embedding vs Referencing
The biggest design decision in MongoDB is whether to embed related data or reference it.
Embedding (Denormalized)
Store related data inside the parent document:
const postSchema = new mongoose.Schema({
title: String,
content: String,
comments: [
{
author: String,
body: String,
createdAt: { type: Date, default: Date.now },
},
],
});Use embedding when:
- The related data is always accessed together
- The embedded array will not grow unboundedly
- The data does not need to be queried independently
Referencing (Normalized)
Store a reference (ID) to a document in another collection:
const commentSchema = new mongoose.Schema({
post: { type: mongoose.Schema.Types.ObjectId, ref: "Post" },
author: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
body: String,
});
// Populate references when querying
const comments = await Comment.find({ post: postId })
.populate("author", "name email")
.sort({ createdAt: -1 });Use referencing when:
- The related data is large or accessed independently
- The related data changes frequently
- You need to query the related data on its own
Aggregation Pipeline
MongoDB's aggregation pipeline lets you process data in stages:
const stats = await Order.aggregate([
// Stage 1: Filter
{ $match: { status: "completed" } },
// Stage 2: Group by customer
{
$group: {
_id: "$customerId",
totalSpent: { $sum: "$total" },
orderCount: { $count: {} },
avgOrderValue: { $avg: "$total" },
},
},
// Stage 3: Sort by total spent
{ $sort: { totalSpent: -1 } },
// Stage 4: Limit to top 10
{ $limit: 10 },
]);Practical Exercise
Build a blog schema with MongoDB:
const blogPostSchema = new mongoose.Schema(
{
title: { type: String, required: true },
slug: { type: String, unique: true, required: true },
content: { type: String, required: true },
author: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
tags: [{ type: String, lowercase: true }],
status: {
type: String,
enum: ["draft", "published", "archived"],
default: "draft",
},
views: { type: Number, default: 0 },
comments: [
{
user: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
body: { type: String, required: true },
createdAt: { type: Date, default: Date.now },
},
],
},
{ timestamps: true }
);
// Indexes for common queries
blogPostSchema.index({ slug: 1 });
blogPostSchema.index({ tags: 1 });
blogPostSchema.index({ status: 1, createdAt: -1 });Key Takeaways
- MongoDB stores data as flexible JSON-like documents grouped into collections.
- Mongoose adds schema validation and convenience methods on top of the native driver.
- Choose embedding for tightly coupled data and referencing for independent, large, or frequently changing data.
- Query operators like
$gt,$in,$or, and$regexgive you powerful filtering capabilities. - The aggregation pipeline processes data in stages for complex analytics queries.