NestJS i MongoDB w 2026: Mongoose, Agregacje i Pytania Rekrutacyjne

Opanuj NestJS z MongoDB i Mongoose 9. Poznaj wzorce projektowania schematów, potoki agregacji i przygotuj się do rozmów kwalifikacyjnych z praktycznymi przykładami.

Framework NestJS z integracją bazy danych MongoDB pokazujący architekturę kodu i potoki agregacji

NestJS 12 w połączeniu z MongoDB poprzez Mongoose 9 stanowi produkcyjnie gotowy stos do budowania skalowalnych backendów w Node.js. Ten przewodnik obejmuje wzorce projektowania schematów, potoki agregacji oraz pytania rekrutacyjne, które odróżniają seniorów od juniorów.

Szybki Przewodnik

Mongoose 9.9.4 wymaga Node.js 18+ i wspiera MongoDB od wersji 6.0 do 8.0. NestJS 12 dostarcza pakiety gotowe na ESM, pozostając jednocześnie wstecznie kompatybilnym z projektami CommonJS.

Konfiguracja Mongoose w Aplikacji NestJS 12

Pakiet @nestjs/mongoose integruje Mongoose z wstrzykiwaniem zależności NestJS. Należy zainstalować wymagane zależności:

bash
# Install Mongoose integration
npm install @nestjs/mongoose mongoose

Rejestracja połączenia w głównym 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 {}

Metoda forRoot akceptuje wszystkie opcje połączenia Mongoose. Ustawienie maxPoolSize zapobiega wyczerpaniu połączeń pod obciążeniem, co jest częstym problemem produkcyjnym.

Projektowanie Schematów z Dekoratorami TypeScript

Schematy Mongoose w NestJS wykorzystują dekoratory z @nestjs/mongoose. Każdy schemat mapuje się na kolekcję 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);

Dekorator @Prop definiuje ograniczenia pól. Ustawienie index: true na często odpytywanych polach poprawia wydajność odczytu kosztem wolniejszych zapisów.

Wskazówka Rekrutacyjna

Rekruterzy pytają o kompromis między osadzonymi dokumentami a referencjami. Osadzone dokumenty sprawdzają się przy danych pobieranych razem (profil użytkownika + preferencje). Referencje pasują do danych, które rosną nieograniczenie lub wymagają niezależnych zapytań (użytkownik + zamówienia).

Wzorzec Repository z Wstrzykiwanymi Serwisami

NestJS zachęca do separacji logiki bazodanowej w serwisach. Dekorator @InjectModel zapewnia dostęp do modelu Mongoose.

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();
  }
}

Wywołanie .exec() zwraca właściwą Promise zamiast obiektu Mongoose Query. Ma to znaczenie dla poprawnego zachowania async/await i śladów stosu błędów.

Potoki Agregacji dla Złożonych Zapytań

Agregacje MongoDB obsługują raportowanie, analitykę i transformacje danych, które bazy SQL rozwiązują za pomocą JOIN i 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();
  }
}

Potoki agregacji przetwarzają dokumenty przez etapy sekwencyjnie. Każdy etap transformuje wyjście dla następnego etapu. Etap $match filtruje wcześnie, aby zredukować liczbę dokumentów przetwarzanych przez kolejne etapy.

Gotowy na rozmowy o Node.js / NestJS?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Transakcje dla Operacji Wielodokumentowych

MongoDB 4.0+ wspiera wielodokumentowe transakcje ACID. Transakcji należy używać, gdy wiele dokumentów musi być aktualizowanych atomowo.

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 automatycznie obsługuje commit i rollback. Jeśli jakakolwiek operacja rzuci wyjątek, cała transakcja zostaje przerwana.

Uwaga Produkcyjna

Transakcje wymagają replica set MongoDB lub klastra shardowanego. Samodzielne instancje MongoDB nie wspierają transakcji. Warstwy Atlas M0/M2/M5 domyślnie zawierają replica sets.

Strategie Indeksowania dla Wydajności Zapytań

Indeksy determinują wydajność zapytań. Bez odpowiednich indeksów MongoDB skanuje całe kolekcje.

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' });

Indeksy złożone wspierają zapytania filtrujące lub sortujące po wielu polach. Indeks { category: 1, price: -1 } optymalizuje zapytania typu find({ category }).sort({ price: -1 }).

Typowe Pytania Rekrutacyjne o NestJS i MongoDB

Rozmowy techniczne sprawdzają zarówno zrozumienie koncepcyjne, jak i praktyczne doświadczenie. Te pytania pojawiają się często na stanowiskach seniorskich backend.

P: Jak Mongoose obsługuje pooling połączeń?

Mongoose wewnętrznie utrzymuje pulę połączeń. Opcja maxPoolSize (domyślnie: 100) ogranicza liczbę równoczesnych połączeń. Każda operacja pobiera połączenie z puli, wykonuje się i je zwraca. Pooling połączeń eliminuje narzut tworzenia nowych połączeń TCP na każde zapytanie.

P: Kiedy należy używać osadzonych dokumentów zamiast referencji?

Osadzaj dane, które należą do siebie i mają ograniczony wzrost. Adresy wysyłkowe użytkownika (maksymalnie 5-10) dobrze się osadzają. Pozycje zamówienia osadzają się w dokumencie zamówienia. Referencje pasują do nieograniczonych relacji: zamówienia użytkownika przez lata lub produkty w kategorii. Zasada: jeśli dane ładują się razem w 90% przypadków i mieszczą się w 16MB, osadź je.

P: Wyjaśnij etap $lookup w potoku agregacji.

Etap $lookup wykonuje left outer join między kolekcjami. Dopasowuje dokumenty z obcej kolekcji na podstawie równości pól lub niestandardowego potoku. W odróżnieniu od SQL join, $lookup wykonuje się podczas agregacji i może zawierać dodatkowe filtrowanie i projekcję wewnątrz joina.

typescript
// Example: Orders with customer details
const pipeline: PipelineStage[] = [
  {
    $lookup: {
      from: 'customers',
      localField: 'customerId',
      foreignField: '_id',
      as: 'customer',
    },
  },
  { $unwind: '$customer' },
];

P: Jak obsługiwać migracje schematów w MongoDB?

Schematy MongoDB ewoluują inaczej niż SQL. Typowe strategie:

  • Dodawanie nowych pól z wartościami domyślnymi (wstecznie kompatybilne)
  • Uruchamianie skryptów migracji aktualizujących istniejące dokumenty partiami
  • Użycie wersjonowania schematów z polem schemaVersion
  • Middleware Mongoose (pre('save')) może transformować dokumenty podczas zapisu
Wskazówka Rekrutacyjna

Kandydaci seniorzy wyjaśniają kompromisy. Juniorzy wymieniają funkcje. Pytani o osadzone vs referencyjne dokumenty, dyskutuj wzorce zapytań, limity rozmiaru dokumentów (16MB) i częstotliwość aktualizacji, nie tylko "to zależy".

Wzorce Obsługi Błędów i Walidacji

Walidacja Mongoose uruchamia się przed operacjami zapisu. Niestandardowe walidatory obsługują logikę biznesową.

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') uruchamia się przed każdą operacją zapisu. Służy do pól wyliczanych, logowania audytu lub kaskadowych aktualizacji.

Monitorowanie Wydajności z explain()

Metoda explain() ujawnia plany wykonania zapytań. Służy do identyfikacji brakujących indeksów i wolnych zapytań.

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
}

Stosunek totalDocsExamined do nReturned bliski 1 wskazuje na efektywne użycie indeksu. Wysokie wartości sygnalizują brakujące indeksy lub nieselektywne zapytania.

Kluczowe Wnioski dla Rozwoju NestJS MongoDB

  • Konfiguruj maxPoolSize w oparciu o oczekiwaną współbieżność, domyślne 100 sprawdza się w większości obciążeń
  • Używaj HydratedDocument<T> dla prawidłowego typowania TypeScript dokumentów Mongoose
  • Wywołuj .exec() na zapytaniach, aby uzyskać natywne Promise z dokładnymi śladami stosu
  • Umieszczaj etapy $match wcześnie w potokach agregacji, aby zredukować liczbę przetwarzanych dokumentów
  • Transakcje wymagają replica sets, zweryfikuj topologię wdrożenia przed poleganiem na nich
  • Twórz indeksy złożone pasujące do wzorców zapytań: pola filtrujące najpierw, pola sortujące potem
  • Osadzaj dokumenty z ograniczonym wzrostem, referencjonuj dokumenty rosnące nieograniczenie
  • Monitoruj wydajność zapytań za pomocą explain('executionStats') podczas rozwoju

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Wyzwanie dnia

Znajdziesz błąd w Node.js / NestJS?

Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Anthony Fillion-Maillet

Autor:

Anthony Fillion-Maillet

Założyciel SharpSkill

Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.

Zaktualizowano 28 sierpnia 2026

Tagi

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

Udostępnij

Powiązane artykuły