NestJS และ MongoDB ในปี 2026: Mongoose, Aggregation และคำถามสัมภาษณ์

เชี่ยวชาญ NestJS กับ MongoDB และ Mongoose 9 เรียนรู้การออกแบบ schema, aggregation pipeline และเตรียมตัวสัมภาษณ์เทคนิคด้วยตัวอย่างจริง

การผสานรวม NestJS framework กับฐานข้อมูล MongoDB แสดงสถาปัตยกรรมโค้ดและ aggregation pipeline

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:

app.module.tstypescript
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

user.schema.tstypescript
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument, Types } from 'mongoose';

// Document type for TypeScript autocomplete
export type UserDocument = HydratedDocument<User>;

@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

user.service.tstypescript
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<UserDocument>,
  ) {}

  async findById(id: string): Promise<UserDocument> {
    // 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<UserDocument | null> {
    // Case-insensitive email lookup
    return this.userModel.findOne({ 
      email: { $regex: new RegExp(`^${email}$`, 'i') } 
    }).exec();
  }

  async create(data: Partial<User>): Promise<UserDocument> {
    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

analytics.service.tstypescript
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<OrderDocument>,
  ) {}

  async getRevenueByMonth(year: number): Promise<MonthlyRevenue[]> {
    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 ที่ถูกประมวลผลโดยขั้นตอนถัดไป

พร้อมที่จะพิชิตการสัมภาษณ์ Node.js / NestJS แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

Transaction สำหรับการดำเนินการหลาย Document

MongoDB 4.0+ รองรับ ACID transaction หลาย document ใช้ transaction เมื่อหลาย document ต้องอัปเดตแบบ atomic

transfer.service.tstypescript
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<AccountDocument>,
  ) {}

  async transfer(
    fromId: string,
    toId: string,
    amount: number,
  ): Promise<void> {
    // 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

product.schema.tstypescript
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';

export type ProductDocument = HydratedDocument<Product>;

@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 ที่กำหนดเองจัดการลอจิกธุรกิจ

order.schema.tstypescript
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';

export type OrderDocument = HydratedDocument<Order>;

@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<void> {
  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<T> สำหรับ 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') ระหว่างการพัฒนา

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

ชาเลนจ์ประจำวัน

คุณหาบั๊กใน Node.js / NestJS เจอไหม

โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 28 สิงหาคม 2569

แท็ก

#nestjs
#mongodb
#mongoose
#nodejs
#backend
#interview

แชร์

บทความที่เกี่ยวข้อง