# NestJS dan MongoDB di 2026: Mongoose, Agregasi, dan Pertanyaan Interview > Kuasai NestJS dengan MongoDB dan Mongoose 9. Pelajari desain schema, pipeline agregasi, dan persiapan interview teknis dengan contoh praktis. - Published: 2026-08-28 - Updated: 2026-08-28 - Author: Anthony Fillion-Maillet - Tags: nestjs, mongodb, mongoose, nodejs, backend, interview - Reading time: 9 min --- NestJS 12 yang dikombinasikan dengan MongoDB melalui Mongoose 9 menyediakan stack yang siap produksi untuk membangun backend Node.js yang skalabel. Panduan ini membahas pola desain schema, pipeline agregasi, dan pertanyaan interview yang membedakan kandidat senior dari junior. > **Referensi Cepat** > > Mongoose 9.9.4 membutuhkan Node.js 18+ dan mendukung MongoDB 6.0 hingga 8.0. NestJS 12 menyediakan paket yang siap ESM namun tetap kompatibel dengan proyek CommonJS. ## Konfigurasi Mongoose dalam Aplikasi NestJS 12 Paket `@nestjs/mongoose` mengintegrasikan Mongoose dengan dependency injection NestJS. Instalasi dependensi yang diperlukan: ```bash # Install Mongoose integration npm install @nestjs/mongoose mongoose ``` Registrasi koneksi pada module root: ```typescript // app.module.ts import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; @Module({ imports: [ MongooseModule.forRoot(process.env.MONGODB_URI, { // Connection pool size for production workloads maxPoolSize: 10, // Timeout after 10 seconds if connection fails serverSelectionTimeoutMS: 10000, }), ], }) export class AppModule {} ``` Method `forRoot` menerima semua opsi koneksi Mongoose. Pengaturan `maxPoolSize` mencegah kelelahan koneksi saat beban tinggi, masalah umum di lingkungan produksi. ## Desain Schema dengan Decorator TypeScript Schema Mongoose di NestJS menggunakan decorator dari `@nestjs/mongoose`. Setiap schema dipetakan ke koleksi MongoDB. ```typescript // user.schema.ts import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { HydratedDocument, Types } from 'mongoose'; // Document type for TypeScript autocomplete export type UserDocument = HydratedDocument; @Schema({ timestamps: true, // Adds createdAt and updatedAt collection: 'users', // Explicit collection name }) export class User { // MongoDB ObjectId, auto-generated _id: Types.ObjectId; @Prop({ required: true, unique: true, index: true }) email: string; @Prop({ required: true }) passwordHash: string; @Prop({ type: String, enum: ['admin', 'user', 'guest'], default: 'user' }) role: string; @Prop({ type: [String], default: [] }) permissions: string[]; } export const UserSchema = SchemaFactory.createForClass(User); ``` Decorator `@Prop` mendefinisikan constraint field. Pengaturan `index: true` pada field yang sering di-query meningkatkan performa baca dengan mengorbankan kecepatan tulis. > **Wawasan Interview** > > Pewawancara sering menanyakan trade-off antara embedded document dan referensi. Embedded document cocok untuk data yang diakses bersamaan (profil pengguna + preferensi). Referensi cocok untuk data yang tumbuh tak terbatas atau membutuhkan query independen (pengguna + pesanan). ## Pola Repository dengan Injectable Service NestJS mendorong pemisahan logika database ke dalam service. Decorator `@InjectModel` menyediakan akses ke model Mongoose. ```typescript // user.service.ts import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model, Types } from 'mongoose'; import { User, UserDocument } from './user.schema'; @Injectable() export class UserService { constructor( @InjectModel(User.name) private userModel: Model, ) {} async findById(id: string): Promise { // Validate ObjectId format before querying if (!Types.ObjectId.isValid(id)) { throw new NotFoundException('Invalid user ID format'); } const user = await this.userModel.findById(id).exec(); if (!user) { throw new NotFoundException(`User ${id} not found`); } return user; } async findByEmail(email: string): Promise { // Case-insensitive email lookup return this.userModel.findOne({ email: { $regex: new RegExp(`^${email}$`, 'i') } }).exec(); } async create(data: Partial): Promise { const user = new this.userModel(data); return user.save(); } } ``` Pemanggilan `.exec()` mengembalikan Promise yang benar alih-alih objek Query Mongoose. Hal ini penting untuk perilaku async/await yang tepat dan stack trace error yang akurat. ## Pipeline Agregasi untuk Query Kompleks Agregasi MongoDB menangani pelaporan, analitik, dan transformasi data yang pada database SQL diselesaikan dengan join dan GROUP BY. ```typescript // analytics.service.ts import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model, PipelineStage } from 'mongoose'; import { Order, OrderDocument } from './order.schema'; @Injectable() export class AnalyticsService { constructor( @InjectModel(Order.name) private orderModel: Model, ) {} async getRevenueByMonth(year: number): Promise { const pipeline: PipelineStage[] = [ // Stage 1: Filter orders by year { $match: { createdAt: { $gte: new Date(`${year}-01-01`), $lt: new Date(`${year + 1}-01-01`), }, status: 'completed', }, }, // Stage 2: Group by month, sum revenue { $group: { _id: { $month: '$createdAt' }, totalRevenue: { $sum: '$amount' }, orderCount: { $sum: 1 }, avgOrderValue: { $avg: '$amount' }, }, }, // Stage 3: Sort by month ascending { $sort: { _id: 1 } }, // Stage 4: Reshape output { $project: { _id: 0, month: '$_id', totalRevenue: { $round: ['$totalRevenue', 2] }, orderCount: 1, avgOrderValue: { $round: ['$avgOrderValue', 2] }, }, }, ]; return this.orderModel.aggregate(pipeline).exec(); } } ``` Pipeline agregasi memproses dokumen melalui tahapan secara berurutan. Setiap tahap mentransformasi output untuk tahap berikutnya. Tahap `$match` memfilter di awal untuk mengurangi dokumen yang diproses oleh tahap selanjutnya. ## Transaksi untuk Operasi Multi-Dokumen MongoDB 4.0+ mendukung transaksi ACID multi-dokumen. Gunakan transaksi ketika beberapa dokumen harus diperbarui secara atomik. ```typescript // transfer.service.ts import { Injectable, BadRequestException } from '@nestjs/common'; import { InjectConnection, InjectModel } from '@nestjs/mongoose'; import { Connection, Model, ClientSession } from 'mongoose'; import { Account, AccountDocument } from './account.schema'; @Injectable() export class TransferService { constructor( @InjectConnection() private connection: Connection, @InjectModel(Account.name) private accountModel: Model, ) {} async transfer( fromId: string, toId: string, amount: number, ): Promise { // Start a session for the transaction const session: ClientSession = await this.connection.startSession(); try { await session.withTransaction(async () => { // Debit source account const source = await this.accountModel.findOneAndUpdate( { _id: fromId, balance: { $gte: amount } }, { $inc: { balance: -amount } }, { session, new: true }, ); if (!source) { throw new BadRequestException('Insufficient balance or account not found'); } // Credit destination account const dest = await this.accountModel.findByIdAndUpdate( toId, { $inc: { balance: amount } }, { session, new: true }, ); if (!dest) { throw new BadRequestException('Destination account not found'); } }); } finally { await session.endSession(); } } } ``` Wrapper `session.withTransaction` menangani commit dan rollback secara otomatis. Jika ada operasi yang throw error, seluruh transaksi dibatalkan. > **Catatan Produksi** > > Transaksi membutuhkan replica set MongoDB atau cluster sharded. Instance MongoDB standalone tidak mendukung transaksi. Tier Atlas M0/M2/M5 sudah menyertakan replica set secara default. ## Strategi Indexing untuk Performa Query Index menentukan performa query. Tanpa index yang tepat, MongoDB melakukan scan seluruh koleksi. ```typescript // product.schema.ts import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { HydratedDocument } from 'mongoose'; export type ProductDocument = HydratedDocument; @Schema({ timestamps: true }) export class Product { @Prop({ required: true, index: true }) sku: string; @Prop({ required: true }) name: string; @Prop({ required: true }) category: string; @Prop({ required: true }) price: number; @Prop({ default: true }) inStock: boolean; } export const ProductSchema = SchemaFactory.createForClass(Product); // Compound index for common query pattern ProductSchema.index({ category: 1, price: -1 }); // Text index for search functionality ProductSchema.index({ name: 'text', sku: 'text' }); ``` Compound index mendukung query yang memfilter atau mengurutkan berdasarkan beberapa field. Index `{ category: 1, price: -1 }` mengoptimalkan query seperti `find({ category }).sort({ price: -1 })`. ## Pertanyaan Interview Umum tentang NestJS dan MongoDB Interview teknis menguji pemahaman konseptual dan pengalaman praktis. Pertanyaan-pertanyaan ini sering muncul untuk posisi backend senior. **Q: Bagaimana Mongoose menangani connection pooling?** Mongoose memelihara connection pool secara internal. Opsi `maxPoolSize` (default: 100) membatasi koneksi bersamaan. Setiap operasi mengambil koneksi dari pool, mengeksekusi, dan mengembalikannya. Connection pooling menghindari overhead pembuatan koneksi TCP baru per query. **Q: Kapan sebaiknya menggunakan embedded document dibanding referensi?** Embed data yang saling terkait dan memiliki pertumbuhan terbatas. Alamat pengiriman pengguna (maksimal 5-10) cocok untuk di-embed. Item pesanan di-embed dalam dokumen pesanan. Referensi cocok untuk relasi tak terbatas: pesanan pengguna selama bertahun-tahun, atau produk dalam kategori. Aturannya: jika data dimuat bersama 90% waktu dan tetap di bawah 16MB, embed saja. **Q: Jelaskan stage $lookup dalam pipeline agregasi.** `$lookup` melakukan left outer join antar koleksi. Stage ini mencocokkan dokumen dari koleksi asing berdasarkan kesetaraan field atau pipeline kustom. Tidak seperti join SQL, `$lookup` dieksekusi selama agregasi dan dapat menyertakan filtering dan projection tambahan dalam join. ```typescript // Example: Orders with customer details const pipeline: PipelineStage[] = [ { $lookup: { from: 'customers', localField: 'customerId', foreignField: '_id', as: 'customer', }, }, { $unwind: '$customer' }, ]; ``` **Q: Bagaimana menangani migrasi schema di MongoDB?** Schema MongoDB berkembang berbeda dari SQL. Strategi umum: - Tambahkan field baru dengan nilai default (backward compatible) - Jalankan script migrasi yang memperbarui dokumen yang ada secara batch - Gunakan versioning schema dengan field `schemaVersion` - Mongoose middleware (`pre('save')`) dapat mentransformasi dokumen saat write > **Tips Interview** > > Kandidat senior menjelaskan trade-off. Junior hanya menyebutkan fitur. Ketika ditanya tentang embedded vs referensi, diskusikan pola query, batas ukuran dokumen (16MB), dan frekuensi update, bukan hanya "tergantung". ## Pola Error Handling dan Validasi Validasi Mongoose berjalan sebelum operasi save. Validator kustom menangani logika bisnis. ```typescript // order.schema.ts import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { HydratedDocument } from 'mongoose'; export type OrderDocument = HydratedDocument; @Schema({ timestamps: true }) export class Order { @Prop({ required: true, validate: { validator: (v: number) => v > 0, message: 'Amount must be positive', }, }) amount: number; @Prop({ type: String, enum: ['pending', 'processing', 'completed', 'cancelled'], default: 'pending', }) status: string; @Prop({ required: true }) items: OrderItem[]; } export const OrderSchema = SchemaFactory.createForClass(Order); // Pre-save hook for computed fields OrderSchema.pre('save', function (next) { // Recalculate total from items if (this.isModified('items')) { this.amount = this.items.reduce( (sum, item) => sum + item.price * item.quantity, 0, ); } next(); }); ``` Middleware `pre('save')` berjalan sebelum setiap operasi save. Gunakan untuk field yang dihitung, audit logging, atau cascading update. ## Monitoring Performa dengan explain() Method `explain()` mengungkapkan rencana eksekusi query. Gunakan untuk mengidentifikasi index yang hilang dan query yang lambat. ```typescript // Debug query performance async analyzeQuery(category: string): Promise { const explanation = await this.productModel .find({ category, inStock: true }) .sort({ price: -1 }) .explain('executionStats'); console.log('Documents examined:', explanation.executionStats.totalDocsExamined); console.log('Documents returned:', explanation.executionStats.nReturned); console.log('Execution time (ms):', explanation.executionStats.executionTimeMillis); // If totalDocsExamined >> nReturned, add an index } ``` Rasio `totalDocsExamined` terhadap `nReturned` yang mendekati 1 menunjukkan penggunaan index yang efisien. Rasio tinggi menandakan index yang hilang atau query yang tidak selektif. ## Poin Penting untuk Pengembangan NestJS MongoDB - Konfigurasi `maxPoolSize` berdasarkan konkurensi yang diharapkan, default 100 cocok untuk sebagian besar workload - Gunakan `HydratedDocument` untuk typing TypeScript yang tepat pada dokumen Mongoose - Panggil `.exec()` pada query untuk mendapatkan Promise native dengan stack trace yang akurat - Letakkan tahap `$match` di awal pipeline agregasi untuk mengurangi dokumen yang diproses - Transaksi membutuhkan replica set, verifikasi topologi deployment sebelum mengandalkannya - Buat compound index yang sesuai dengan pola query: field filter dulu, field sort kedua - Embed dokumen dengan pertumbuhan terbatas, referensikan dokumen yang tumbuh tak terbatas - Monitor performa query dengan `explain('executionStats')` selama development --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/node-nestjs/nestjs-mongodb-mongoose-guide