# NestJS และ MongoDB ในปี 2026: Mongoose, Aggregation และคำถามสัมภาษณ์ > เชี่ยวชาญ NestJS กับ MongoDB และ Mongoose 9 เรียนรู้การออกแบบ schema, aggregation pipeline และเตรียมตัวสัมภาษณ์เทคนิคด้วยตัวอย่างจริง - 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 ผสานกับ MongoDB ผ่าน Mongoose 9 ให้ stack ที่พร้อมใช้งานจริงสำหรับการสร้าง backend Node.js ที่ปรับขนาดได้ คู่มือนี้ครอบคลุมรูปแบบการออกแบบ schema, aggregation pipeline และคำถามสัมภาษณ์ที่แยกผู้สมัคร senior ออกจาก junior > **อ้างอิงด่วน** > > Mongoose 9.9.4 ต้องการ Node.js 18+ และรองรับ MongoDB 6.0 ถึง 8.0 NestJS 12 มาพร้อม package ที่พร้อมใช้ ESM แต่ยังคงเข้ากันได้กับโปรเจกต์ CommonJS ## การตั้งค่า Mongoose ในแอปพลิเคชัน NestJS 12 Package `@nestjs/mongoose` ผสาน Mongoose เข้ากับ dependency injection ของ NestJS ติดตั้ง dependency ที่จำเป็น: ```bash # Install Mongoose integration npm install @nestjs/mongoose mongoose ``` ลงทะเบียนการเชื่อมต่อใน root module: ```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 {} ``` เมธอด `forRoot` รับตัวเลือกการเชื่อมต่อ Mongoose ทั้งหมด การตั้งค่า `maxPoolSize` ป้องกันการหมดการเชื่อมต่อภายใต้โหลดสูง ซึ่งเป็นปัญหาทั่วไปใน production ## การออกแบบ Schema ด้วย TypeScript Decorator Mongoose schema ใน NestJS ใช้ decorator จาก `@nestjs/mongoose` แต่ละ schema จะ map ไปยัง 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` กำหนดข้อจำกัดของ field การตั้งค่า `index: true` บน field ที่ถูก query บ่อยจะปรับปรุงประสิทธิภาพการอ่านแต่ทำให้การเขียนช้าลง > **มุมมองสัมภาษณ์** > > ผู้สัมภาษณ์มักถามเกี่ยวกับ trade-off ระหว่าง embedded document และ reference Embedded document เหมาะกับข้อมูลที่เข้าถึงพร้อมกัน (โปรไฟล์ผู้ใช้ + การตั้งค่า) Reference เหมาะกับข้อมูลที่เติบโตไม่จำกัดหรือต้องการ query แยก (ผู้ใช้ + คำสั่งซื้อ) ## รูปแบบ Repository ด้วย Injectable Service NestJS ส่งเสริมการแยกลอจิก database ออกเป็น service Decorator `@InjectModel` ให้การเข้าถึง 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(); } } ``` การเรียก `.exec()` จะคืน Promise ที่ถูกต้องแทนที่จะเป็น Mongoose Query object สิ่งนี้สำคัญสำหรับพฤติกรรม async/await ที่ถูกต้องและ stack trace ของ error ที่แม่นยำ ## Aggregation Pipeline สำหรับ Query ที่ซับซ้อน MongoDB aggregation จัดการรายงาน การวิเคราะห์ และการแปลงข้อมูลที่ฐานข้อมูล SQL แก้ด้วย join และ 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 ประมวลผล document ผ่านขั้นตอนตามลำดับ แต่ละขั้นตอนแปลง output สำหรับขั้นตอนถัดไป ขั้นตอน `$match` กรองตั้งแต่ต้นเพื่อลด document ที่ถูกประมวลผลโดยขั้นตอนถัดไป ## Transaction สำหรับการดำเนินการหลาย Document MongoDB 4.0+ รองรับ ACID transaction หลาย document ใช้ transaction เมื่อหลาย document ต้องอัปเดตแบบ atomic ```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` จัดการ commit และ rollback โดยอัตโนมัติ หากการดำเนินการใดๆ throw error ทั้ง transaction จะถูกยกเลิก > **หมายเหตุ Production** > > Transaction ต้องการ replica set MongoDB หรือ sharded cluster Instance MongoDB standalone ไม่รองรับ transaction Atlas tier M0/M2/M5 รวม replica set เป็นค่าเริ่มต้น ## กลยุทธ์ Indexing สำหรับประสิทธิภาพ Query Index กำหนดประสิทธิภาพ query หากไม่มี index ที่เหมาะสม MongoDB จะ scan ทั้ง 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 รองรับ query ที่กรองหรือเรียงลำดับตามหลาย field Index `{ category: 1, price: -1 }` เพิ่มประสิทธิภาพ query เช่น `find({ category }).sort({ price: -1 })` ## คำถามสัมภาษณ์ทั่วไปเกี่ยวกับ NestJS และ MongoDB การสัมภาษณ์เทคนิคทดสอบทั้งความเข้าใจแนวคิดและประสบการณ์จริง คำถามเหล่านี้ปรากฏบ่อยสำหรับตำแหน่ง backend senior **Q: Mongoose จัดการ connection pooling อย่างไร?** Mongoose รักษา connection pool ภายใน ตัวเลือก `maxPoolSize` (ค่าเริ่มต้น: 100) จำกัดการเชื่อมต่อพร้อมกัน แต่ละการดำเนินการจะเช็คเอาต์การเชื่อมต่อจาก pool ดำเนินการ และคืน Connection pooling หลีกเลี่ยงค่าใช้จ่ายของการสร้างการเชื่อมต่อ TCP ใหม่ต่อ query **Q: เมื่อไหร่ควรใช้ embedded document แทน reference?** Embed ข้อมูลที่เป็นของกันและมีการเติบโตจำกัด ที่อยู่จัดส่งของผู้ใช้ (สูงสุด 5-10) embed ได้ดี รายการในคำสั่งซื้อ embed ภายใน document คำสั่งซื้อ Reference เหมาะกับความสัมพันธ์ไม่จำกัด: คำสั่งซื้อของผู้ใช้ตลอดหลายปี หรือสินค้าในหมวดหมู่ กฎ: หากข้อมูลโหลดพร้อมกัน 90% ของเวลาและต่ำกว่า 16MB ให้ embed **Q: อธิบาย stage $lookup ใน aggregation pipeline** `$lookup` ทำ left outer join ระหว่าง collection มันจับคู่ document จาก collection ต่างประเทศตามความเท่าเทียมของ field หรือ pipeline ที่กำหนดเอง ไม่เหมือน join ของ SQL `$lookup` ดำเนินการระหว่าง aggregation และสามารถรวมการกรองและ projection เพิ่มเติมภายใน join ```typescript // Example: Orders with customer details const pipeline: PipelineStage[] = [ { $lookup: { from: 'customers', localField: 'customerId', foreignField: '_id', as: 'customer', }, }, { $unwind: '$customer' }, ]; ``` **Q: จัดการ schema migration ใน MongoDB อย่างไร?** Schema MongoDB พัฒนาแตกต่างจาก SQL กลยุทธ์ทั่วไป: - เพิ่ม field ใหม่ด้วยค่าเริ่มต้น (เข้ากันได้ย้อนหลัง) - รันสคริปต์ migration ที่อัปเดต document ที่มีอยู่เป็น batch - ใช้ versioning schema ด้วย field `schemaVersion` - Mongoose middleware (`pre('save')`) สามารถแปลง document เมื่อเขียน > **เคล็ดลับสัมภาษณ์** > > ผู้สมัคร senior อธิบาย trade-off Junior แค่ระบุฟีเจอร์ เมื่อถูกถามเกี่ยวกับ embedded vs reference ให้พูดคุยเกี่ยวกับรูปแบบ query ขีดจำกัดขนาด document (16MB) และความถี่ในการอัปเดต ไม่ใช่แค่ "ขึ้นอยู่กับ" ## รูปแบบ Error Handling และ Validation Mongoose validation ทำงานก่อนการดำเนินการ save Validator ที่กำหนดเองจัดการลอจิกธุรกิจ ```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')` ทำงานก่อนการดำเนินการ save แต่ละครั้ง ใช้สำหรับ field ที่คำนวณ audit logging หรือ cascading update ## การตรวจสอบประสิทธิภาพด้วย explain() เมธอด `explain()` เปิดเผยแผนการดำเนินการ query ใช้เพื่อระบุ index ที่หายไปและ query ที่ช้า ```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 } ``` อัตราส่วน `totalDocsExamined` ต่อ `nReturned` ที่ใกล้ 1 บ่งบอกการใช้ index ที่มีประสิทธิภาพ อัตราส่วนสูงบ่งบอก index ที่หายไปหรือ query ที่ไม่เลือกสรร ## ประเด็นสำคัญสำหรับการพัฒนา NestJS MongoDB - กำหนดค่า `maxPoolSize` ตามความพร้อมกันที่คาดหวัง ค่าเริ่มต้น 100 เหมาะกับ workload ส่วนใหญ่ - ใช้ `HydratedDocument` สำหรับ TypeScript typing ที่ถูกต้องของ Mongoose document - เรียก `.exec()` บน query เพื่อรับ Promise native พร้อม stack trace ที่แม่นยำ - วาง stage `$match` ตั้งแต่ต้นใน aggregation pipeline เพื่อลด document ที่ประมวลผล - Transaction ต้องการ replica set ตรวจสอบ topology การ deploy ก่อนพึ่งพามัน - สร้าง compound index ที่ตรงกับรูปแบบ query: field กรองก่อน field เรียงลำดับหลัง - Embed document ที่มีการเติบโตจำกัด reference document ที่เติบโตไม่จำกัด - ตรวจสอบประสิทธิภาพ query ด้วย `explain('executionStats')` ระหว่างการพัฒนา --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/node-nestjs/nestjs-mongodb-mongoose-guide