# NestJS và MongoDB trong 2026: Mongoose, Aggregation và Câu hỏi Phỏng vấn > Thành thạo NestJS với MongoDB và Mongoose 9. Học thiết kế schema, aggregation pipeline, và chuẩn bị phỏng vấn kỹ thuật với các ví dụ thực tế. - 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 kết hợp với MongoDB thông qua Mongoose 9 cung cấp một stack sẵn sàng cho production để xây dựng backend Node.js có khả năng mở rộng. Hướng dẫn này trình bày các pattern thiết kế schema, aggregation pipeline, và những câu hỏi phỏng vấn phân biệt ứng viên senior với junior. > **Tham khảo Nhanh** > > Mongoose 9.9.4 yêu cầu Node.js 18+ và hỗ trợ MongoDB 6.0 đến 8.0. NestJS 12 cung cấp các package sẵn sàng ESM nhưng vẫn tương thích ngược với các dự án CommonJS. ## Thiết lập Mongoose trong Ứng dụng NestJS 12 Package `@nestjs/mongoose` tích hợp Mongoose với dependency injection của NestJS. Cài đặt các dependency cần thiết: ```bash # Install Mongoose integration npm install @nestjs/mongoose mongoose ``` Đăng ký kết nối trong module gốc: ```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 {} ``` Phương thức `forRoot` chấp nhận tất cả các tùy chọn kết nối Mongoose. Cấu hình `maxPoolSize` ngăn chặn việc cạn kiệt kết nối dưới tải cao, một vấn đề phổ biến trong môi trường production. ## Thiết kế Schema với TypeScript Decorator Mongoose schema trong NestJS sử dụng decorator từ `@nestjs/mongoose`. Mỗi schema ánh xạ đến một collection 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` định nghĩa các ràng buộc trường. Thiết lập `index: true` trên các trường được query thường xuyên cải thiện hiệu suất đọc nhưng làm chậm ghi. > **Góc nhìn Phỏng vấn** > > Nhà tuyển dụng thường hỏi về sự đánh đổi giữa embedded document và reference. Embedded document phù hợp với dữ liệu được truy cập cùng nhau (hồ sơ người dùng + tùy chọn). Reference phù hợp với dữ liệu tăng trưởng không giới hạn hoặc yêu cầu query độc lập (người dùng + đơn hàng). ## Pattern Repository với Injectable Service NestJS khuyến khích tách logic database vào các service. Decorator `@InjectModel` cung cấp quyền truy cập vào Mongoose model. ```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(); } } ``` Việc gọi `.exec()` trả về một Promise đúng nghĩa thay vì đối tượng Mongoose Query. Điều này quan trọng cho hành vi async/await chính xác và stack trace lỗi rõ ràng. ## Aggregation Pipeline cho Query Phức tạp MongoDB aggregation xử lý báo cáo, phân tích, và biến đổi dữ liệu mà database SQL giải quyết bằng join và 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(); } } ``` Aggregation pipeline xử lý document qua các giai đoạn tuần tự. Mỗi giai đoạn biến đổi output cho giai đoạn tiếp theo. Giai đoạn `$match` lọc sớm để giảm số document được xử lý bởi các giai đoạn sau. ## Transaction cho Thao tác Đa Document MongoDB 4.0+ hỗ trợ transaction ACID đa document. Sử dụng transaction khi nhiều document cần được cập nhật nguyên tử. ```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` xử lý commit và rollback tự động. Nếu bất kỳ thao tác nào throw lỗi, toàn bộ transaction bị hủy. > **Lưu ý Production** > > Transaction yêu cầu replica set MongoDB hoặc sharded cluster. Instance MongoDB standalone không hỗ trợ transaction. Các tier Atlas M0/M2/M5 bao gồm replica set theo mặc định. ## Chiến lược Indexing cho Hiệu suất Query Index quyết định hiệu suất query. Không có index phù hợp, MongoDB quét toàn bộ collection. ```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 hỗ trợ các query lọc hoặc sắp xếp theo nhiều trường. Index `{ category: 1, price: -1 }` tối ưu hóa các query như `find({ category }).sort({ price: -1 })`. ## Câu hỏi Phỏng vấn Phổ biến về NestJS và MongoDB Phỏng vấn kỹ thuật kiểm tra cả hiểu biết khái niệm và kinh nghiệm thực tế. Những câu hỏi này xuất hiện thường xuyên cho các vị trí backend senior. **Q: Mongoose xử lý connection pooling như thế nào?** Mongoose duy trì connection pool nội bộ. Tùy chọn `maxPoolSize` (mặc định: 100) giới hạn các kết nối đồng thời. Mỗi thao tác lấy một kết nối từ pool, thực thi, và trả lại. Connection pooling tránh chi phí thiết lập kết nối TCP mới cho mỗi query. **Q: Khi nào nên sử dụng embedded document thay vì reference?** Embed dữ liệu thuộc về nhau và có tăng trưởng giới hạn. Địa chỉ giao hàng của người dùng (tối đa 5-10) embed tốt. Các mục trong đơn hàng embed trong document đơn hàng. Reference phù hợp với quan hệ không giới hạn: đơn hàng của người dùng qua nhiều năm, hoặc sản phẩm trong danh mục. Quy tắc: nếu dữ liệu tải cùng nhau 90% thời gian và dưới 16MB, hãy embed. **Q: Giải thích giai đoạn $lookup trong aggregation pipeline.** `$lookup` thực hiện left outer join giữa các collection. Nó khớp document từ collection ngoại dựa trên bằng trường hoặc pipeline tùy chỉnh. Không giống join SQL, `$lookup` thực thi trong quá trình aggregation và có thể bao gồm lọc và projection bổ sung trong join. ```typescript // Example: Orders with customer details const pipeline: PipelineStage[] = [ { $lookup: { from: 'customers', localField: 'customerId', foreignField: '_id', as: 'customer', }, }, { $unwind: '$customer' }, ]; ``` **Q: Làm thế nào để xử lý schema migration trong MongoDB?** Schema MongoDB phát triển khác với SQL. Các chiến lược phổ biến: - Thêm trường mới với giá trị mặc định (tương thích ngược) - Chạy script migration cập nhật document hiện có theo batch - Sử dụng versioning schema với trường `schemaVersion` - Mongoose middleware (`pre('save')`) có thể biến đổi document khi ghi > **Mẹo Phỏng vấn** > > Ứng viên senior giải thích các đánh đổi. Junior chỉ liệt kê tính năng. Khi được hỏi về embedded vs reference, thảo luận về pattern query, giới hạn kích thước document (16MB), và tần suất cập nhật, không chỉ "tùy thuộc". ## Pattern Xử lý Lỗi và Validation Mongoose validation chạy trước các thao tác save. Validator tùy chỉnh xử lý logic nghiệp vụ. ```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')` chạy trước mỗi thao tác save. Sử dụng cho các trường tính toán, audit logging, hoặc cascading update. ## Giám sát Hiệu suất với explain() Phương thức `explain()` tiết lộ kế hoạch thực thi query. Sử dụng để xác định index bị thiếu và query chậm. ```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 } ``` Tỷ lệ `totalDocsExamined` trên `nReturned` gần 1 cho thấy sử dụng index hiệu quả. Tỷ lệ cao báo hiệu index bị thiếu hoặc query không chọn lọc. ## Điểm Chính cho Phát triển NestJS MongoDB - Cấu hình `maxPoolSize` dựa trên mức độ đồng thời dự kiến, mặc định 100 phù hợp với hầu hết workload - Sử dụng `HydratedDocument` cho TypeScript typing chính xác trên Mongoose document - Gọi `.exec()` trên query để nhận Promise native với stack trace chính xác - Đặt giai đoạn `$match` sớm trong aggregation pipeline để giảm document được xử lý - Transaction yêu cầu replica set, xác minh topology deployment trước khi dựa vào chúng - Tạo compound index khớp với pattern query: trường lọc trước, trường sắp xếp sau - Embed document có tăng trưởng giới hạn, reference document tăng trưởng không giới hạn - Giám sát hiệu suất query với `explain('executionStats')` trong quá trình phát triển --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/node-nestjs/nestjs-mongodb-mongoose-guide