NestJS e MongoDB nel 2026: Mongoose, Aggregazioni e Domande da Colloquio

Guida pratica a NestJS 12 con MongoDB e Mongoose 9: design degli schema, pipeline di aggregazione, transazioni e domande tecniche per colloqui backend.

NestJS e MongoDB nel 2026: Mongoose, Aggregazioni e Domande da Colloquio

NestJS 12 combinato con MongoDB tramite Mongoose 9 fornisce uno stack pronto per la produzione per costruire backend Node.js scalabili. Questa guida copre i pattern di design degli schema, le pipeline di aggregazione e le domande da colloquio che distinguono i candidati senior dai junior.

Riferimento Rapido

Mongoose 9.9.4 richiede Node.js 18+ e supporta MongoDB dalla versione 6.0 alla 8.0. NestJS 12 distribuisce pacchetti pronti per ESM ma rimane retrocompatibile con i progetti CommonJS.

Configurazione di Mongoose in un'Applicazione NestJS 12

Il pacchetto @nestjs/mongoose integra Mongoose con la dependency injection di NestJS. L'installazione delle dipendenze richieste avviene tramite npm:

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

La connessione viene registrata nel modulo root:

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 {}

Il metodo forRoot accetta tutte le opzioni di connessione Mongoose. L'impostazione maxPoolSize previene l'esaurimento delle connessioni sotto carico, un problema comune in produzione.

Design degli Schema con Decoratori TypeScript

Gli schema Mongoose in NestJS utilizzano decoratori da @nestjs/mongoose. Ogni schema viene mappato a una 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);

Il decoratore @Prop definisce i vincoli dei campi. Impostare index: true sui campi interrogati frequentemente migliora le prestazioni di lettura a scapito di scritture più lente.

Approfondimento da Colloquio

Gli intervistatori chiedono del trade-off tra documenti incorporati e riferimenti. I documenti incorporati sono adatti per dati accessibili insieme (profilo utente + preferenze). I riferimenti sono appropriati per dati che crescono illimitatamente o richiedono query indipendenti (utente + ordini).

Pattern Repository con Servizi Injectable

NestJS incoraggia la separazione della logica database in servizi. Il decoratore @InjectModel fornisce accesso al model 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();
  }
}

Chiamare .exec() restituisce una Promise nativa invece di un oggetto Query Mongoose. Questo è importante per il corretto comportamento async/await e gli stack trace degli errori.

Pipeline di Aggregazione per Query Complesse

Le aggregazioni MongoDB gestiscono reporting, analytics e trasformazioni dati che i database SQL risolvono con join e 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();
  }
}

Le pipeline di aggregazione processano i documenti sequenzialmente attraverso stadi. Ogni stadio trasforma l'output per lo stadio successivo. Lo stadio $match filtra presto per ridurre i documenti processati dagli stadi successivi.

Pronto a superare i tuoi colloqui su Node.js / NestJS?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Transazioni per Operazioni Multi-Documento

MongoDB 4.0+ supporta transazioni ACID multi-documento. Le transazioni vengono utilizzate quando più documenti devono essere aggiornati atomicamente.

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

Il wrapper session.withTransaction gestisce commit e rollback automaticamente. Se qualsiasi operazione lancia un'eccezione, l'intera transazione viene annullata.

Nota per la Produzione

Le transazioni richiedono un replica set MongoDB o un cluster sharded. Le istanze MongoDB standalone non supportano le transazioni. I tier Atlas M0/M2/M5 includono replica set di default.

Strategie di Indicizzazione per le Prestazioni delle Query

Gli indici determinano le prestazioni delle query. Senza indici appropriati, MongoDB scansiona intere 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' });

Gli indici composti supportano query che filtrano o ordinano per campi multipli. L'indice { category: 1, price: -1 } ottimizza query come find({ category }).sort({ price: -1 }).

Domande Comuni da Colloquio su NestJS e MongoDB

I colloqui tecnici testano sia la comprensione concettuale che l'esperienza pratica. Queste domande appaiono frequentemente per posizioni backend senior.

D: Come gestisce Mongoose il connection pooling?

Mongoose mantiene internamente un connection pool. L'opzione maxPoolSize (default: 100) limita le connessioni concorrenti. Ogni operazione preleva una connessione dal pool, la esegue e la restituisce. Il connection pooling evita l'overhead di stabilire nuove connessioni TCP per ogni query.

D: Quando utilizzare documenti incorporati invece di riferimenti?

Incorporare dati che appartengono insieme e hanno crescita limitata. Gli indirizzi di spedizione di un utente (massimo 5-10) si incorporano bene. Le righe d'ordine vengono incorporate nel documento dell'ordine. I riferimenti sono adatti per relazioni illimitate: gli ordini di un utente nel corso degli anni, o i prodotti in una categoria. La regola: se i dati vengono caricati insieme il 90% delle volte e restano sotto i 16MB, incorporarli.

D: Spiegare lo stadio $lookup della pipeline di aggregazione.

Lo stadio $lookup esegue un left outer join tra collection. Effettua il match di documenti da una collection esterna basandosi sull'uguaglianza dei campi o una pipeline personalizzata. A differenza dei join SQL, $lookup viene eseguito durante l'aggregazione e può includere filtraggio e proiezione aggiuntivi all'interno del join.

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

D: Come vengono gestite le migrazioni degli schema in MongoDB?

Gli schema MongoDB evolvono diversamente da SQL. Strategie comuni:

  • Aggiungere nuovi campi con valori di default (retrocompatibile)
  • Eseguire script di migrazione che aggiornano i documenti esistenti in batch
  • Utilizzare il versionamento degli schema con un campo schemaVersion
  • Il middleware Mongoose (pre('save')) può trasformare i documenti in scrittura
Suggerimento per il Colloquio

I candidati senior spiegano i trade-off. I junior elencano le feature. Quando viene chiesto di documenti incorporati vs riferimenti, discutere i pattern di query, i limiti di dimensione dei documenti (16MB) e la frequenza di aggiornamento, non solo "dipende".

Gestione degli Errori e Pattern di Validazione

La validazione Mongoose viene eseguita prima delle operazioni di salvataggio. I validatori personalizzati gestiscono la logica di business.

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

Il middleware pre('save') viene eseguito prima di ogni operazione di salvataggio. Viene utilizzato per campi calcolati, audit logging o aggiornamenti a cascata.

Monitoraggio delle Prestazioni con explain()

Il metodo explain() rivela i piani di esecuzione delle query. Viene utilizzato per identificare indici mancanti e query lente.

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
}

Un rapporto di totalDocsExamined rispetto a nReturned vicino a 1 indica un utilizzo efficiente degli indici. Rapporti elevati segnalano indici mancanti o query non selettive.

Punti Chiave per lo Sviluppo NestJS MongoDB

  • Configurare maxPoolSize in base alla concorrenza attesa, il default 100 è adatto per la maggior parte dei workload
  • Utilizzare HydratedDocument<T> per il typing TypeScript corretto dei documenti Mongoose
  • Chiamare .exec() sulle query per ottenere Promise native con stack trace accurati
  • Posizionare gli stadi $match presto nelle pipeline di aggregazione per ridurre i documenti processati
  • Le transazioni richiedono replica set, verificare la topologia del deployment prima di dipenderne
  • Creare indici composti che corrispondano ai pattern delle query: campi di filtro prima, campi di ordinamento dopo
  • Incorporare documenti con crescita limitata, riferire documenti che crescono illimitatamente
  • Monitorare le prestazioni delle query con explain('executionStats') durante lo sviluppo

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Sfida del giorno

Sapresti trovare il bug in Node.js / NestJS?

Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Anthony Fillion-Maillet

Scritto da

Anthony Fillion-Maillet

Fondatore di SharpSkill

Sviluppatore fullstack da oltre 10 anni. Guida SharpSkill e risponde di tutto ciò che vi viene pubblicato.

Aggiornato il 28 agosto 2026

Condividi

Articoli correlati