Kuasai MongoDB dari konsep inti hingga produksi. Pelajari documents, collections, indexing, aggregations, dan bangun platform social media lengkap dengan NestJS featuring posts, comments, followers, dan real-time analytics.

Aplikasi modern memerlukan flexible data models. Requirements berubah rapidly, dan rigid database schemas menjadi bottlenecks. MongoDB menyelesaikan ini dengan menyediakan flexible, document-oriented database yang scale horizontally dan adapt ke evolving data structures.
Digunakan oleh companies seperti Uber, Airbnb, dan Slack, MongoDB power applications handling billions dari documents. Ini bukan hanya database—ini adalah platform yang enable rapid development, horizontal scaling, dan complex data operations melalui aggregation pipelines.
Dalam artikel ini, kita akan mengeksplorasi arsitektur MongoDB, memahami setiap core concept dari documents hingga aggregations, dan membangun production-ready social media platform dengan NestJS yang mendemonstrasikan flexible schemas, complex queries, dan real-time analytics.
Traditional SQL databases memiliki limitations untuk modern applications:
Rigid Schemas: Schema changes memerlukan migrations. Menambahkan fields ke millions dari records lambat.
Complex Joins: Normalizing data memerlukan multiple tables dan expensive joins.
Vertical Scaling: Limited oleh single server capacity. Horizontal scaling complex.
Impedance Mismatch: Objects tidak map cleanly ke relational tables.
Fixed Structure: Tidak bisa handle semi-structured atau evolving data.
MongoDB dibangun untuk modern applications:
Flexible Schemas: Tambahkan fields tanpa migrations. Different documents bisa have different structures.
Document Model: Data stored sebagai JSON-like documents. Natural object mapping.
Horizontal Scaling: Sharding distribute data di multiple servers.
Rich Queries: Complex queries tanpa joins melalui aggregation pipelines.
Rapid Development: Iterate quickly tanpa schema changes.
Database: Container untuk collections. Similar dengan database di SQL.
Collection: Group dari documents. Similar dengan table di SQL.
Document: Single record stored sebagai BSON (Binary JSON). Similar dengan row di SQL.
Field: Key-value pair dalam document. Similar dengan column di SQL.
Index: Data structure untuk fast queries. Similar dengan SQL indexes.
Shard: Partition dari data di servers. Enable horizontal scaling.
Replica Set: Multiple copies dari data untuk redundancy dan read scaling.
Application → Driver → MongoDB Server → Storage Engine → DiskMongoDB store documents sebagai 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 adalah 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:
# Buat 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 untuk full-text search
db.posts.createIndex({ title: "text", content: "text" })
# Geospatial index
db.locations.createIndex({ coordinates: "2dsphere" })
# TTL index (auto-delete setelah expiration)
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })Index Strategies:
# Ascending (1) vs Descending (-1)
# Gunakan ascending untuk equality, descending untuk 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 provide powerful query language.
Basic Queries:
# Find all
db.users.find({})
# Find dengan filter
db.users.find({ age: { $gt: 25 } })
# Find dengan multiple conditions
db.users.find({
age: { $gte: 25, $lte: 35 },
status: "active"
})
# Find dengan regex
db.users.find({ email: { $regex: "@gmail.com$" } })
# Find dengan 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 untuk 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 dan aggregate
$sort # Sort documents
$limit # Limit results
$skip # Skip documents
$project # Reshape documents
$lookup # Join dengan other collections
$unwind # Flatten arrays
$facet # Multiple pipelines
$out # Write ke collectionUse Cases:
ACID transactions untuk 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 dan 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 dari secondary
db.getMongo().setReadPref("secondary")Use Cases:
Horizontal scaling dengan 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
# Jangan gunakan: { status: 1 } # Hanya "active" atau "inactive"Use Cases:
Schema validation untuk 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 dari 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:
Sekarang mari kita bangun production-ready social media platform yang mendemonstrasikan MongoDB patterns. Sistem handle:
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 sebelum 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 untuk 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);
// Buat 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);
// Buat 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 {
// Tambahkan ke following
await this.userModel.findByIdAndUpdate(
userId,
{
$addToSet: { following: targetUserId },
$inc: { followingCount: 1 },
},
{ session },
);
// Tambahkan ke 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;
// Dapatkan user's following list
const user = await this.userModel.findById(userId);
const followingIds = user.following;
// Dapatkan posts dari 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();
// Tambahkan comment ke 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' }] },
],
},
},
},
]);
}
}[Sama seperti versi English - struktur identik dengan terjemahan komentar]
# Buat user
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{
"email": "alice@example.com",
"username": "alice",
"password": "password123"
}'
# Dapatkan 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"}'
# Buat post
curl -X POST http://localhost:3000/posts \
-H "Content-Type: application/json" \
-d '{
"userId": "USER_ID",
"content": "Hello, world!",
"images": []
}'
# Dapatkan user feed
curl "http://localhost:3000/posts/feed/USER_ID?page=1&limit=10"
# Dapatkan 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"}'
# Tambahkan comment
curl -X POST http://localhost:3000/posts/POST_ID/comments \
-H "Content-Type: application/json" \
-d '{
"userId": "USER_ID",
"content": "Great post!"
}'
# Dapatkan post analytics
curl http://localhost:3000/posts/POST_ID/analytics
# Dapatkan followers
curl http://localhost:3000/users/USER_ID/followers
# Dapatkan following
curl http://localhost:3000/users/USER_ID/followingQueries tanpa indexes lambat di large collections.
// ❌ Salah - tidak ada index
db.users.find({ email: "user@example.com" })
// ✅ Benar - buat index
db.users.createIndex({ email: 1 })
db.users.find({ email: "user@example.com" })Arrays yang grow unbounded cause performance issues.
// ❌ Salah - array grow indefinitely
{
_id: 1,
comments: [comment1, comment2, ..., comment1000000]
}
// ✅ Benar - gunakan separate collection
// posts collection
{ _id: 1, commentsCount: 1000000 }
// comments collection
{ postId: 1, content: "..." }Multi-step operations tanpa transactions bisa leave data inconsistent.
// ❌ Salah - tidak ada transaction
await users.updateOne({ _id: 1 }, { $inc: { balance: -100 } })
await users.updateOne({ _id: 2 }, { $inc: { balance: 100 } })
// Jika second fail, data inconsistent
// ✅ Benar - gunakan 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 tanpa proper stages lambat.
// ❌ Salah - inefficient
db.posts.aggregate([
{ $lookup: { from: "users", ... } },
{ $lookup: { from: "comments", ... } },
{ $match: { status: "published" } } // Filter setelah joins
])
// ✅ Benar - filter early
db.posts.aggregate([
{ $match: { status: "published" } }, // Filter first
{ $lookup: { from: "users", ... } },
{ $lookup: { from: "comments", ... } }
])Connection failures harus 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 bisa corrupt database Anda.
// ✅ Gunakan schema validation
UserSchema.pre('save', async function (next) {
if (!this.email.includes('@')) {
throw new Error('Invalid email')
}
next()
})Balance antara normalization dan 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 yang digunakan di queries dan sorts.
// ✅ Buat indexes untuk common queries
db.posts.createIndex({ userId: 1, createdAt: -1 })
db.posts.createIndex({ status: 1 })
db.comments.createIndex({ postId: 1, createdAt: -1 })Avoid loading semua documents sekaligus.
// ✅ 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 untuk complex queries.
// ✅ Gunakan aggregation untuk analytics
const stats = await this.postModel.aggregate([
{ $match: { status: "published" } },
{ $group: { _id: "$userId", count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 10 }
])Track slow queries dan 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 untuk better performance.
// ✅ Connection pooling configured di 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 |
Pilih MongoDB ketika:
Pilih PostgreSQL ketika:
Pilih Cassandra ketika:
MongoDB adalah powerful document database yang enable rapid development dan horizontal scaling. Memahami core concepts—documents, collections, indexing, dan aggregations—mengaktifkan Anda untuk build scalable applications.
Contoh social media platform mendemonstrasikan production patterns:
Key takeaways:
Mulai dengan simple collections dan queries. Seiring complexity tumbuh, explore advanced patterns seperti sharding, replica sets, dan change streams. Fleksibilitas MongoDB membuatnya suitable untuk systems dari simple CRUD applications hingga complex analytics platforms.
Langkah selanjutnya:
MongoDB mentransformasi bagaimana Anda think tentang data modeling—dari rigid schemas ke flexible documents. Master it, dan Anda akan bangun applications yang scale dengan business Anda.