Master MongoDB from core concepts to production. Learn documents, collections, indexing, aggregations, and build a complete social media platform with NestJS featuring posts, comments, followers, and real-time analytics.

Modern applications need flexible data models. Requirements change rapidly, and rigid database schemas become bottlenecks. MongoDB solves this by providing a flexible, document-oriented database that scales horizontally and adapts to evolving data structures.
Used by companies like Uber, Airbnb, and Slack, MongoDB powers applications handling billions of documents. It's not just a database—it's a platform that enables rapid development, horizontal scaling, and complex data operations through aggregation pipelines.
In this article, we'll explore MongoDB's architecture, understand every core concept from documents to aggregations, and build a production-ready social media platform with NestJS that demonstrates flexible schemas, complex queries, and real-time analytics.
Traditional SQL databases have limitations for modern applications:
Rigid Schemas: Schema changes require migrations. Adding fields to millions of records is slow.
Complex Joins: Normalizing data requires multiple tables and expensive joins.
Vertical Scaling: Limited by single server capacity. Horizontal scaling is complex.
Impedance Mismatch: Objects don't map cleanly to relational tables.
Fixed Structure: Can't handle semi-structured or evolving data.
MongoDB was built for modern applications:
Flexible Schemas: Add fields without migrations. Different documents can have different structures.
Document Model: Data stored as JSON-like documents. Natural object mapping.
Horizontal Scaling: Sharding distributes data across multiple servers.
Rich Queries: Complex queries without joins through aggregation pipelines.
Rapid Development: Iterate quickly without schema changes.
Database: Container for collections. Similar to a database in SQL.
Collection: Group of documents. Similar to a table in SQL.
Document: Single record stored as BSON (Binary JSON). Similar to a row in SQL.
Field: Key-value pair within a document. Similar to a column in SQL.
Index: Data structure for fast queries. Similar to SQL indexes.
Shard: Partition of data across servers. Enables horizontal scaling.
Replica Set: Multiple copies of data for redundancy and read scaling.
Application → Driver → MongoDB Server → Storage Engine → DiskMongoDB stores documents as BSON (Binary JSON):
{
"_id": ObjectId("507f1f77bcf86cd799439011"),
"name": "John Doe",
"email": "john@example.com",
"age": 30,
"tags": ["developer", "mongodb"],
"address": {
"street": "123 Main St",
"city": "New York"
},
"created_at": ISODate("2026-02-25T10:00:00Z")
}Documents are flexible JSON-like objects.
Document Structure:
db.users.insertOne({
_id: ObjectId(),
name: "Alice",
email: "alice@example.com",
age: 28,
tags: ["python", "data-science"],
profile: {
bio: "Data scientist",
avatar: "https://example.com/avatar.jpg"
},
created_at: new Date()
})Collection Operations:
# Create collection
db.createCollection("users")
# List collections
db.getCollectionNames()
# Drop collection
db.users.drop()
# Get collection stats
db.users.stats()Use Cases:
Indexes speed up queries dramatically.
Index Types:
# Single field index
db.users.createIndex({ email: 1 })
# Compound index
db.posts.createIndex({ userId: 1, createdAt: -1 })
# Text index for full-text search
db.posts.createIndex({ title: "text", content: "text" })
# Geospatial index
db.locations.createIndex({ coordinates: "2dsphere" })
# TTL index (auto-delete after expiration)
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })Index Strategies:
# Ascending (1) vs Descending (-1)
# Use ascending for equality, descending for sorting
# Compound indexes follow ESR rule:
# E - Equality fields first
# S - Sort fields second
# R - Range fields last
db.orders.createIndex({
status: 1, # Equality
createdAt: -1, # Sort
amount: 1 # Range
})Use Cases:
MongoDB provides powerful query language.
Basic Queries:
# Find all
db.users.find({})
# Find with filter
db.users.find({ age: { $gt: 25 } })
# Find with multiple conditions
db.users.find({
age: { $gte: 25, $lte: 35 },
status: "active"
})
# Find with regex
db.users.find({ email: { $regex: "@gmail.com$" } })
# Find with array contains
db.users.find({ tags: "mongodb" })Query Operators:
# Comparison
$eq, $ne, $gt, $gte, $lt, $lte
# Logical
$and, $or, $not, $nor
# Array
$in, $nin, $all, $elemMatch
# Element
$exists, $type
# Evaluation
$regex, $text, $whereUse Cases:
Powerful framework for data transformation.
Pipeline Stages:
db.posts.aggregate([
# Stage 1: Filter
{ $match: { status: "published" } },
# Stage 2: Group
{ $group: {
_id: "$userId",
postCount: { $sum: 1 },
avgLikes: { $avg: "$likes" }
}
},
# Stage 3: Sort
{ $sort: { postCount: -1 } },
# Stage 4: Limit
{ $limit: 10 }
])Common Stages:
$match # Filter documents
$group # Group and aggregate
$sort # Sort documents
$limit # Limit results
$skip # Skip documents
$project # Reshape documents
$lookup # Join with other collections
$unwind # Flatten arrays
$facet # Multiple pipelines
$out # Write to collectionUse Cases:
ACID transactions for multi-document operations.
Transaction Example:
session = db.getMongo().startSession()
session.startTransaction()
try {
db.accounts.updateOne(
{ _id: 1 },
{ $inc: { balance: -100 } },
{ session }
)
db.accounts.updateOne(
{ _id: 2 },
{ $inc: { balance: 100 } },
{ session }
)
session.commitTransaction()
} catch (error) {
session.abortTransaction()
throw error
}Use Cases:
Replica sets provide redundancy and read scaling.
Replica Set Architecture:
Primary (writes) ← Replication → Secondary (reads)
↓
Secondary (reads)Configuration:
# Initialize replica set
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "mongo1:27017" },
{ _id: 1, host: "mongo2:27017" },
{ _id: 2, host: "mongo3:27017" }
]
})
# Check status
rs.status()
# Read from secondary
db.getMongo().setReadPref("secondary")Use Cases:
Horizontal scaling by distributing data.
Sharding Architecture:
Application → Mongos (Router) → Shard 1 (data range A-M)
→ Shard 2 (data range N-Z)Shard Key Selection:
# Good shard key: high cardinality, evenly distributed
db.adminCommand({
shardCollection: "mydb.users",
key: { userId: 1 }
})
# Bad shard key: low cardinality, uneven distribution
# Don't use: { status: 1 } # Only "active" or "inactive"Use Cases:
Schema validation for data quality.
Validation Rules:
db.createCollection("users", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["email", "name"],
properties: {
_id: { bsonType: "objectId" },
email: {
bsonType: "string",
pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
},
name: { bsonType: "string" },
age: {
bsonType: "int",
minimum: 0,
maximum: 150
},
tags: {
bsonType: "array",
items: { bsonType: "string" }
}
}
}
}
})Use Cases:
Real-time notifications of data changes.
Change Stream Example:
const changeStream = db.users.watch([
{ $match: { operationType: "insert" } }
])
changeStream.on("change", (change) => {
console.log("New user:", change.fullDocument)
})Use Cases:
Efficient batch operations.
Bulk Write:
const bulk = db.users.initializeUnorderedBulkOp()
bulk.insert({ name: "Alice", age: 28 })
bulk.insert({ name: "Bob", age: 32 })
bulk.find({ _id: 1 }).updateOne({ $set: { age: 29 } })
bulk.find({ _id: 2 }).removeOne()
bulk.execute()Use Cases:
Now let's build a production-ready social media platform that demonstrates MongoDB patterns. The system handles:
npm i -g @nestjs/cli
nest new social-media-platform
cd social-media-platform
npm install @nestjs/mongoose mongoose class-validator class-transformer bcryptjsimport { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [
MongooseModule.forRoot(
process.env.MONGODB_URI || 'mongodb://localhost:27017/social-media',
{
useNewUrlParser: true,
useUnifiedTopology: true,
},
),
],
})
export class DatabaseModule {}import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
import * as bcrypt from 'bcryptjs';
@Schema({ timestamps: true })
export class User extends Document {
@Prop({ required: true, unique: true })
email: string;
@Prop({ required: true })
username: string;
@Prop({ required: true })
password: string;
@Prop()
bio?: string;
@Prop()
avatar?: string;
@Prop({ type: [Types.ObjectId], ref: 'User', default: [] })
followers: Types.ObjectId[];
@Prop({ type: [Types.ObjectId], ref: 'User', default: [] })
following: Types.ObjectId[];
@Prop({ default: 0 })
postsCount: number;
@Prop({ default: 0 })
followersCount: number;
@Prop({ default: 0 })
followingCount: number;
@Prop({ default: true })
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}
export const UserSchema = SchemaFactory.createForClass(User);
// Hash password before saving
UserSchema.pre('save', async function (next) {
if (!this.isModified('password')) return next();
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// Add method to compare passwords
UserSchema.methods.comparePassword = async function (password: string) {
return bcrypt.compare(password, this.password);
};import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
@Schema({ timestamps: true })
export class Post extends Document {
@Prop({ type: Types.ObjectId, ref: 'User', required: true })
userId: Types.ObjectId;
@Prop({ required: true })
content: string;
@Prop({ type: [String], default: [] })
images: string[];
@Prop({ type: [Types.ObjectId], ref: 'User', default: [] })
likes: Types.ObjectId[];
@Prop({ default: 0 })
likesCount: number;
@Prop({ default: 0 })
commentsCount: number;
@Prop({ default: 0 })
sharesCount: number;
@Prop({ type: [Types.ObjectId], ref: 'Comment', default: [] })
comments: Types.ObjectId[];
@Prop({ default: 'public', enum: ['public', 'private', 'friends'] })
visibility: string;
createdAt: Date;
updatedAt: Date;
}
export const PostSchema = SchemaFactory.createForClass(Post);
// Create indexes
PostSchema.index({ userId: 1, createdAt: -1 });
PostSchema.index({ createdAt: -1 });
PostSchema.index({ 'likes': 1 });import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
@Schema({ timestamps: true })
export class Comment extends Document {
@Prop({ type: Types.ObjectId, ref: 'Post', required: true })
postId: Types.ObjectId;
@Prop({ type: Types.ObjectId, ref: 'User', required: true })
userId: Types.ObjectId;
@Prop({ required: true })
content: string;
@Prop({ type: [Types.ObjectId], ref: 'User', default: [] })
likes: Types.ObjectId[];
@Prop({ default: 0 })
likesCount: number;
createdAt: Date;
updatedAt: Date;
}
export const CommentSchema = SchemaFactory.createForClass(Comment);
// Create indexes
CommentSchema.index({ postId: 1, createdAt: -1 });
CommentSchema.index({ userId: 1 });import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { User } from '../schemas/user.schema';
@Injectable()
export class UsersService {
constructor(@InjectModel(User.name) private userModel: Model<User>) {}
async createUser(email: string, username: string, password: string) {
const user = new this.userModel({ email, username, password });
return user.save();
}
async getUserById(id: string) {
return this.userModel.findById(id).select('-password');
}
async getUserByEmail(email: string) {
return this.userModel.findOne({ email });
}
async getUserProfile(userId: string) {
return this.userModel.findById(userId).select('-password').populate('followers following');
}
async followUser(userId: string, targetUserId: string) {
const session = await this.userModel.startSession();
session.startTransaction();
try {
// Add to following
await this.userModel.findByIdAndUpdate(
userId,
{
$addToSet: { following: targetUserId },
$inc: { followingCount: 1 },
},
{ session },
);
// Add to followers
await this.userModel.findByIdAndUpdate(
targetUserId,
{
$addToSet: { followers: userId },
$inc: { followersCount: 1 },
},
{ session },
);
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
}
async unfollowUser(userId: string, targetUserId: string) {
const session = await this.userModel.startSession();
session.startTransaction();
try {
await this.userModel.findByIdAndUpdate(
userId,
{
$pull: { following: targetUserId },
$inc: { followingCount: -1 },
},
{ session },
);
await this.userModel.findByIdAndUpdate(
targetUserId,
{
$pull: { followers: userId },
$inc: { followersCount: -1 },
},
{ session },
);
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
}
async getFollowers(userId: string, limit: number = 20) {
return this.userModel
.findById(userId)
.populate({
path: 'followers',
select: 'username avatar bio',
options: { limit },
});
}
async getFollowing(userId: string, limit: number = 20) {
return this.userModel
.findById(userId)
.populate({
path: 'following',
select: 'username avatar bio',
options: { limit },
});
}
}import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { Post } from '../schemas/post.schema';
import { Comment } from '../schemas/comment.schema';
import { User } from '../schemas/user.schema';
@Injectable()
export class PostsService {
constructor(
@InjectModel(Post.name) private postModel: Model<Post>,
@InjectModel(Comment.name) private commentModel: Model<Comment>,
@InjectModel(User.name) private userModel: Model<User>,
) {}
async createPost(userId: string, content: string, images: string[] = []) {
const post = new this.postModel({
userId,
content,
images,
});
const savedPost = await post.save();
// Increment user's post count
await this.userModel.findByIdAndUpdate(userId, {
$inc: { postsCount: 1 },
});
return savedPost;
}
async getPostById(postId: string) {
return this.postModel
.findById(postId)
.populate('userId', 'username avatar')
.populate({
path: 'comments',
populate: { path: 'userId', select: 'username avatar' },
});
}
async getUserFeed(userId: string, page: number = 1, limit: number = 10) {
const skip = (page - 1) * limit;
// Get user's following list
const user = await this.userModel.findById(userId);
const followingIds = user.following;
// Get posts from following users
return this.postModel
.find({
userId: { $in: followingIds },
visibility: { $in: ['public', 'friends'] },
})
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit)
.populate('userId', 'username avatar')
.populate({
path: 'comments',
options: { limit: 3 },
populate: { path: 'userId', select: 'username avatar' },
});
}
async likePost(postId: string, userId: string) {
return this.postModel.findByIdAndUpdate(
postId,
{
$addToSet: { likes: userId },
$inc: { likesCount: 1 },
},
{ new: true },
);
}
async unlikePost(postId: string, userId: string) {
return this.postModel.findByIdAndUpdate(
postId,
{
$pull: { likes: userId },
$inc: { likesCount: -1 },
},
{ new: true },
);
}
async addComment(postId: string, userId: string, content: string) {
const comment = new this.commentModel({
postId,
userId,
content,
});
const savedComment = await comment.save();
// Add comment to post
await this.postModel.findByIdAndUpdate(postId, {
$push: { comments: savedComment._id },
$inc: { commentsCount: 1 },
});
return savedComment;
}
async getTrendingPosts(limit: number = 10) {
return this.postModel.aggregate([
{
$match: { visibility: 'public' },
},
{
$addFields: {
score: {
$add: [
{ $multiply: ['$likesCount', 2] },
'$commentsCount',
{ $multiply: ['$sharesCount', 3] },
],
},
},
},
{
$sort: { score: -1, createdAt: -1 },
},
{
$limit: limit,
},
{
$lookup: {
from: 'users',
localField: 'userId',
foreignField: '_id',
as: 'user',
},
},
{
$unwind: '$user',
},
{
$project: {
content: 1,
images: 1,
likesCount: 1,
commentsCount: 1,
sharesCount: 1,
score: 1,
createdAt: 1,
'user.username': 1,
'user.avatar': 1,
},
},
]);
}
async getPostAnalytics(postId: string) {
return this.postModel.aggregate([
{
$match: { _id: new Types.ObjectId(postId) },
},
{
$lookup: {
from: 'comments',
localField: '_id',
foreignField: 'postId',
as: 'allComments',
},
},
{
$project: {
content: 1,
likesCount: 1,
commentsCount: 1,
sharesCount: 1,
createdAt: 1,
totalEngagement: {
$add: ['$likesCount', '$commentsCount', '$sharesCount'],
},
engagementRate: {
$divide: [
{ $add: ['$likesCount', '$commentsCount', '$sharesCount'] },
{ $max: [1, { $size: '$allComments' }] },
],
},
},
},
]);
}
}import { Controller, Post, Get, Put, Delete, Body, Param, Query } from '@nestjs/common';
import { PostsService } from './posts.service';
@Controller('posts')
export class PostsController {
constructor(private readonly postsService: PostsService) {}
@Post()
async createPost(
@Body() createPostDto: { userId: string; content: string; images?: string[] },
) {
const post = await this.postsService.createPost(
createPostDto.userId,
createPostDto.content,
createPostDto.images,
);
return {
message: 'Post created successfully',
post,
};
}
@Get('feed/:userId')
async getUserFeed(
@Param('userId') userId: string,
@Query('page') page: number = 1,
@Query('limit') limit: number = 10,
) {
const posts = await this.postsService.getUserFeed(userId, page, limit);
return {
page,
limit,
posts,
};
}
@Get('trending')
async getTrendingPosts(@Query('limit') limit: number = 10) {
const posts = await this.postsService.getTrendingPosts(limit);
return {
count: posts.length,
posts,
};
}
@Get(':id')
async getPost(@Param('id') id: string) {
return this.postsService.getPostById(id);
}
@Get(':id/analytics')
async getPostAnalytics(@Param('id') id: string) {
return this.postsService.getPostAnalytics(id);
}
@Put(':id/like')
async likePost(
@Param('id') postId: string,
@Body() likeDto: { userId: string },
) {
const post = await this.postsService.likePost(postId, likeDto.userId);
return {
message: 'Post liked',
post,
};
}
@Put(':id/unlike')
async unlikePost(
@Param('id') postId: string,
@Body() unlikeDto: { userId: string },
) {
const post = await this.postsService.unlikePost(postId, unlikeDto.userId);
return {
message: 'Post unliked',
post,
};
}
@Post(':id/comments')
async addComment(
@Param('id') postId: string,
@Body() commentDto: { userId: string; content: string },
) {
const comment = await this.postsService.addComment(
postId,
commentDto.userId,
commentDto.content,
);
return {
message: 'Comment added',
comment,
};
}
}import { Controller, Post, Get, Put, Body, Param } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
async createUser(
@Body() createUserDto: { email: string; username: string; password: string },
) {
const user = await this.usersService.createUser(
createUserDto.email,
createUserDto.username,
createUserDto.password,
);
return {
message: 'User created successfully',
user: { id: user._id, email: user.email, username: user.username },
};
}
@Get(':id')
async getUser(@Param('id') id: string) {
return this.usersService.getUserProfile(id);
}
@Put(':id/follow')
async followUser(
@Param('id') targetUserId: string,
@Body() followDto: { userId: string },
) {
await this.usersService.followUser(followDto.userId, targetUserId);
return {
message: 'User followed successfully',
};
}
@Put(':id/unfollow')
async unfollowUser(
@Param('id') targetUserId: string,
@Body() unfollowDto: { userId: string },
) {
await this.usersService.unfollowUser(unfollowDto.userId, targetUserId);
return {
message: 'User unfollowed successfully',
};
}
@Get(':id/followers')
async getFollowers(@Param('id') userId: string) {
return this.usersService.getFollowers(userId);
}
@Get(':id/following')
async getFollowing(@Param('id') userId: string) {
return this.usersService.getFollowing(userId);
}
}import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { DatabaseModule } from './database/database.module';
import { UsersService } from './users/users.service';
import { UsersController } from './users/users.controller';
import { PostsService } from './posts/posts.service';
import { PostsController } from './posts/posts.controller';
import { User, UserSchema } from './schemas/user.schema';
import { Post, PostSchema } from './schemas/post.schema';
import { Comment, CommentSchema } from './schemas/comment.schema';
@Module({
imports: [
DatabaseModule,
MongooseModule.forFeature([
{ name: User.name, schema: UserSchema },
{ name: Post.name, schema: PostSchema },
{ name: Comment.name, schema: CommentSchema },
]),
],
controllers: [UsersController, PostsController],
providers: [UsersService, PostsService],
})
export class AppModule {}version: '3.8'
services:
mongodb:
image: mongo:7.0
ports:
- '27017:27017'
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: password
volumes:
- mongodb_data:/data/db
mongo-express:
image: mongo-express:latest
ports:
- '8081:8081'
environment:
ME_CONFIG_MONGODB_ADMINUSERNAME: admin
ME_CONFIG_MONGODB_ADMINPASSWORD: password
ME_CONFIG_MONGODB_URL: mongodb://admin:password@mongodb:27017/
depends_on:
- mongodb
volumes:
mongodb_data:# Start MongoDB
docker-compose up -d
# Install dependencies
npm install
# Run application
npm run start:dev
# Access Mongo Express
# http://localhost:8081# Create user
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{
"email": "alice@example.com",
"username": "alice",
"password": "password123"
}'
# Get user profile
curl http://localhost:3000/users/USER_ID
# Follow user
curl -X PUT http://localhost:3000/users/TARGET_USER_ID/follow \
-H "Content-Type: application/json" \
-d '{"userId": "USER_ID"}'
# Create post
curl -X POST http://localhost:3000/posts \
-H "Content-Type: application/json" \
-d '{
"userId": "USER_ID",
"content": "Hello, world!",
"images": []
}'
# Get user feed
curl "http://localhost:3000/posts/feed/USER_ID?page=1&limit=10"
# Get trending posts
curl "http://localhost:3000/posts/trending?limit=10"
# Like post
curl -X PUT http://localhost:3000/posts/POST_ID/like \
-H "Content-Type: application/json" \
-d '{"userId": "USER_ID"}'
# Add comment
curl -X POST http://localhost:3000/posts/POST_ID/comments \
-H "Content-Type: application/json" \
-d '{
"userId": "USER_ID",
"content": "Great post!"
}'
# Get post analytics
curl http://localhost:3000/posts/POST_ID/analytics
# Get followers
curl http://localhost:3000/users/USER_ID/followers
# Get following
curl http://localhost:3000/users/USER_ID/followingQueries without indexes are slow on large collections.
// ❌ Wrong - no index
db.users.find({ email: "user@example.com" })
// ✅ Correct - create index
db.users.createIndex({ email: 1 })
db.users.find({ email: "user@example.com" })Arrays that grow unbounded cause performance issues.
// ❌ Wrong - array grows indefinitely
{
_id: 1,
comments: [comment1, comment2, ..., comment1000000]
}
// ✅ Correct - use separate collection
// posts collection
{ _id: 1, commentsCount: 1000000 }
// comments collection
{ postId: 1, content: "..." }Multi-step operations without transactions can leave data inconsistent.
// ❌ Wrong - no transaction
await users.updateOne({ _id: 1 }, { $inc: { balance: -100 } })
await users.updateOne({ _id: 2 }, { $inc: { balance: 100 } })
// If second fails, data is inconsistent
// ✅ Correct - use transaction
const session = await db.startSession()
session.startTransaction()
try {
await users.updateOne({ _id: 1 }, { $inc: { balance: -100 } }, { session })
await users.updateOne({ _id: 2 }, { $inc: { balance: 100 } }, { session })
await session.commitTransaction()
} catch (error) {
await session.abortTransaction()
}Complex aggregations without proper stages are slow.
// ❌ Wrong - inefficient
db.posts.aggregate([
{ $lookup: { from: "users", ... } },
{ $lookup: { from: "comments", ... } },
{ $match: { status: "published" } } // Filter after joins
])
// ✅ Correct - filter early
db.posts.aggregate([
{ $match: { status: "published" } }, // Filter first
{ $lookup: { from: "users", ... } },
{ $lookup: { from: "comments", ... } }
])Connection failures should be handled gracefully.
// ✅ Proper error handling
try {
const user = await this.userModel.findById(userId)
return user
} catch (error) {
if (error.name === 'MongoNetworkError') {
throw new ServiceUnavailableException('Database unavailable')
}
throw error
}Invalid data can corrupt your database.
// ✅ Use schema validation
UserSchema.pre('save', async function (next) {
if (!this.email.includes('@')) {
throw new Error('Invalid email')
}
next()
})Balance between normalization and denormalization.
// ✅ Good schema design
// Users collection
{ _id: 1, name: "Alice", postsCount: 5 }
// Posts collection
{ _id: 1, userId: 1, content: "...", likesCount: 10 }
// Comments collection
{ _id: 1, postId: 1, userId: 2, content: "..." }Index fields used in queries and sorts.
// ✅ Create indexes for common queries
db.posts.createIndex({ userId: 1, createdAt: -1 })
db.posts.createIndex({ status: 1 })
db.comments.createIndex({ postId: 1, createdAt: -1 })Avoid loading all documents at once.
// ✅ Paginate results
const page = 1
const limit = 20
const skip = (page - 1) * limit
const posts = await this.postModel
.find()
.skip(skip)
.limit(limit)
.sort({ createdAt: -1 })Leverage aggregation for complex queries.
// ✅ Use aggregation for analytics
const stats = await this.postModel.aggregate([
{ $match: { status: "published" } },
{ $group: { _id: "$userId", count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 10 }
])Track slow queries and optimize.
# Enable profiling
db.setProfilingLevel(1, { slowms: 100 })
# View slow queries
db.system.profile.find({ millis: { $gt: 100 } }).pretty()Handle MongoDB-specific errors.
// ✅ Handle MongoDB errors
try {
await this.userModel.create(userData)
} catch (error) {
if (error.code === 11000) {
throw new ConflictException('Email already exists')
}
throw error
}Reuse connections for better performance.
// ✅ Connection pooling configured in MongooseModule
MongooseModule.forRoot(mongoUri, {
maxPoolSize: 10,
minPoolSize: 5,
})| Feature | MongoDB | PostgreSQL | Cassandra |
|---|---|---|---|
| Schema | Flexible | Rigid | Flexible |
| Transactions | Multi-doc | ACID | Limited |
| Joins | Aggregation | Native | No |
| Scalability | Horizontal | Vertical | Horizontal |
| Consistency | Eventual | Strong | Eventual |
| Query Language | MQL | SQL | CQL |
Choose MongoDB when:
Choose PostgreSQL when:
Choose Cassandra when:
MongoDB is a powerful document database that enables rapid development and horizontal scaling. Understanding its core concepts—documents, collections, indexing, and aggregations—enables you to build scalable applications.
The social media platform example demonstrates production patterns:
Key takeaways:
Start with simple collections and queries. As complexity grows, explore advanced patterns like sharding, replica sets, and change streams. MongoDB's flexibility makes it suitable for systems ranging from simple CRUD applications to complex analytics platforms.
Next steps:
MongoDB transforms how you think about data modeling—from rigid schemas to flexible documents. Master it, and you'll build applications that scale with your business.