NestJS et MongoDB en 2026 : Mongoose, Agrégations et Questions d'Entretien

Guide complet sur l'intégration de NestJS 12 avec MongoDB via Mongoose 9. Découvrez les schémas, les pipelines d'agrégation et les questions techniques fréquentes en entretien.

Guide NestJS MongoDB avec Mongoose, agrégations et questions d'entretien

NestJS 12 combiné avec MongoDB via Mongoose 9 constitue une stack robuste pour développer des backends Node.js évolutifs. Ce guide couvre les patterns de conception de schémas, les pipelines d'agrégation et les questions d'entretien qui différencient les développeurs seniors des juniors.

Référence Rapide

Mongoose 9.9.4 nécessite Node.js 18+ et supporte MongoDB 6.0 à 8.0. NestJS 12 fournit des packages compatibles ESM tout en restant rétrocompatible avec les projets CommonJS.

Configuration de Mongoose dans une Application NestJS 12

Le package @nestjs/mongoose intègre Mongoose avec l'injection de dépendances de NestJS. Installation des dépendances requises :

bash
# Installation de l'intégration Mongoose
npm install @nestjs/mongoose mongoose

Enregistrement de la connexion dans le module racine :

app.module.tstypescript
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [
    MongooseModule.forRoot(process.env.MONGODB_URI, {
      // Taille du pool de connexions pour la production
      maxPoolSize: 10,
      // Timeout après 10 secondes si la connexion échoue
      serverSelectionTimeoutMS: 10000,
    }),
  ],
})
export class AppModule {}

La méthode forRoot accepte toutes les options de connexion Mongoose. Configurer maxPoolSize évite l'épuisement des connexions sous charge, un problème fréquent en production.

Conception de Schémas avec les Décorateurs TypeScript

Les schémas Mongoose dans NestJS utilisent les décorateurs de @nestjs/mongoose. Chaque schéma correspond à une collection MongoDB.

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

// Type de document pour l'autocomplétion TypeScript
export type UserDocument = HydratedDocument<User>;

@Schema({
  timestamps: true, // Ajoute createdAt et updatedAt
  collection: 'users', // Nom explicite de la collection
})
export class User {
  // ObjectId MongoDB, généré automatiquement
  _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);

Le décorateur @Prop définit les contraintes des champs. Activer index: true sur les champs fréquemment interrogés améliore les performances de lecture au détriment de l'écriture.

Point d'Entretien

Les recruteurs demandent souvent la différence entre documents imbriqués et références. Les documents imbriqués conviennent aux données consultées ensemble (profil utilisateur + préférences). Les références s'appliquent aux données qui croissent sans limite ou nécessitent des requêtes indépendantes (utilisateur + commandes).

Pattern Repository avec Services Injectables

NestJS encourage la séparation de la logique de base de données dans des services. Le décorateur @InjectModel donne accès au modèle 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> {
    // Validation du format ObjectId avant la requête
    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> {
    // Recherche email insensible à la casse
    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();
  }
}

Appeler .exec() retourne une Promise native au lieu d'un objet Query Mongoose. Cela importe pour le comportement async/await et les traces d'erreurs.

Pipelines d'Agrégation pour Requêtes Complexes

Les agrégations MongoDB gèrent les rapports, analyses et transformations de données que les bases SQL résolvent avec des jointures et 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[] = [
      // Étape 1: Filtrer les commandes par année
      {
        $match: {
          createdAt: {
            $gte: new Date(`${year}-01-01`),
            $lt: new Date(`${year + 1}-01-01`),
          },
          status: 'completed',
        },
      },
      // Étape 2: Grouper par mois, sommer les revenus
      {
        $group: {
          _id: { $month: '$createdAt' },
          totalRevenue: { $sum: '$amount' },
          orderCount: { $sum: 1 },
          avgOrderValue: { $avg: '$amount' },
        },
      },
      // Étape 3: Trier par mois croissant
      { $sort: { _id: 1 } },
      // Étape 4: Reformater la sortie
      {
        $project: {
          _id: 0,
          month: '$_id',
          totalRevenue: { $round: ['$totalRevenue', 2] },
          orderCount: 1,
          avgOrderValue: { $round: ['$avgOrderValue', 2] },
        },
      },
    ];

    return this.orderModel.aggregate(pipeline).exec();
  }
}

Les pipelines d'agrégation traitent les documents séquentiellement à travers les étapes. Chaque étape transforme la sortie pour l'étape suivante. L'étape $match filtre tôt pour réduire les documents traités par les étapes suivantes.

Prêt à réussir tes entretiens Node.js / NestJS ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Transactions pour Opérations Multi-Documents

MongoDB 4.0+ supporte les transactions ACID multi-documents. Les transactions s'utilisent quand plusieurs documents doivent être mis à jour atomiquement.

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> {
    // Démarrer une session pour la transaction
    const session: ClientSession = await this.connection.startSession();

    try {
      await session.withTransaction(async () => {
        // Débiter le compte source
        const fromAccount = await this.accountModel.findByIdAndUpdate(
          fromId,
          { $inc: { balance: -amount } },
          { session, new: true },
        );

        if (!fromAccount || fromAccount.balance < 0) {
          throw new BadRequestException('Solde insuffisant');
        }

        // Créditer le compte destination
        await this.accountModel.findByIdAndUpdate(
          toId,
          { $inc: { balance: amount } },
          { session },
        );
      });
    } finally {
      await session.endSession();
    }
  }
}

La méthode withTransaction gère automatiquement le commit en cas de succès et le rollback si une erreur est levée. Toujours passer l'objet session à chaque opération de la transaction.

Index et Optimisation des Performances

La conception d'index impacte directement la latence des requêtes. MongoDB supporte les index simples, composés et de texte.

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

export type ProductDocument = HydratedDocument<Product>;

@Schema()
export class Product {
  @Prop({ required: true, index: true })
  category: string;

  @Prop({ required: true })
  name: string;

  @Prop({ type: Number, required: true })
  price: number;

  @Prop({ type: Boolean, default: true })
  inStock: boolean;
}

export const ProductSchema = SchemaFactory.createForClass(Product);

// Index composé pour les requêtes fréquentes
ProductSchema.index({ category: 1, price: -1 });

// Index de texte pour la recherche
ProductSchema.index({ name: 'text' });

Les index composés supportent les requêtes utilisant les champs préfixes. Un index sur { category: 1, price: -1 } accélère les requêtes filtrant par catégorie ou par catégorie et prix, mais pas celles filtrant uniquement par prix.

Gestion des Erreurs et Validation

Mongoose fournit une validation intégrée. Combiner avec les pipes NestJS pour une validation robuste des entrées.

create-user.dto.tstypescript
import { IsEmail, IsString, MinLength, IsEnum, IsOptional } from 'class-validator';

export class CreateUserDto {
  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  password: string;

  @IsOptional()
  @IsEnum(['admin', 'user', 'guest'])
  role?: string;
}
user.controller.tstypescript
import { Controller, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './create-user.dto';

@Controller('users')
export class UserController {
  constructor(private userService: UserService) {}

  @Post()
  @UsePipes(new ValidationPipe({ whitelist: true }))
  async create(@Body() dto: CreateUserDto) {
    return this.userService.create(dto);
  }
}

L'option whitelist: true supprime les propriétés non décorées du DTO, empêchant l'injection de champs non autorisés.

Questions d'Entretien Techniques Fréquentes

Les entretiens techniques sur NestJS et MongoDB couvrent architecture, performance et cas limites.

Question : Quand utiliser $lookup versus documents imbriqués ?

Réponse attendue : $lookup effectue une jointure côté serveur entre collections. Utiliser $lookup quand les données référencées sont volumineuses, mises à jour indépendamment, ou requises dans des requêtes séparées. Les documents imbriqués conviennent quand les données sont toujours consultées ensemble et tiennent dans la limite de 16MB du document.

typescript
// Exemple $lookup dans une agrégation
const pipeline = [
  {
    $lookup: {
      from: 'orders',
      localField: '_id',
      foreignField: 'userId',
      as: 'orders',
    },
  },
  {
    $project: {
      email: 1,
      orderCount: { $size: '$orders' },
    },
  },
];

Question : Comment gérer la pagination avec de grands ensembles de données ?

Réponse attendue : Éviter skip() pour les grandes valeurs d'offset car MongoDB doit parcourir tous les documents ignorés. Utiliser la pagination par curseur avec un champ indexé.

typescript
// Pagination inefficace
await this.model.find().skip(10000).limit(20);

// Pagination par curseur (performante)
await this.model
  .find({ _id: { $gt: lastSeenId } })
  .sort({ _id: 1 })
  .limit(20);

Question : Comment sécuriser les requêtes contre l'injection NoSQL ?

Réponse attendue : Mongoose échappe par défaut les requêtes avec $where désactivé. Toujours valider les entrées utilisateur et éviter de passer directement des objets dans les conditions de requête.

typescript
// Vulnérable - l'utilisateur pourrait injecter { $gt: '' }
const user = await this.model.findOne({ email: userInput });

// Sécurisé - forcer le type string
const user = await this.model.findOne({ email: String(userInput) });

Question : Quelle est la différence entre save() et findByIdAndUpdate() ?

Réponse attendue : save() charge le document en mémoire, applique les middlewares (hooks pre/post), et exécute les validateurs. findByIdAndUpdate() envoie une mise à jour directe à MongoDB, contournant les middlewares sauf configuration explicite.

typescript
// Déclenche les middlewares
const user = await this.userModel.findById(id);
user.lastLogin = new Date();
await user.save();

// Contourne les middlewares par défaut
await this.userModel.findByIdAndUpdate(id, { lastLogin: new Date() });

// Pour activer les validateurs
await this.userModel.findByIdAndUpdate(
  id,
  { lastLogin: new Date() },
  { runValidators: true },
);

Tests d'Intégration avec MongoDB Memory Server

Tester les services Mongoose nécessite une instance MongoDB. mongodb-memory-server fournit une base de données en mémoire pour les tests.

user.service.spec.tstypescript
import { Test } from '@nestjs/testing';
import { MongooseModule } from '@nestjs/mongoose';
import { MongoMemoryServer } from 'mongodb-memory-server';
import { UserService } from './user.service';
import { User, UserSchema } from './user.schema';

describe('UserService', () => {
  let service: UserService;
  let mongod: MongoMemoryServer;

  beforeAll(async () => {
    mongod = await MongoMemoryServer.create();
    const uri = mongod.getUri();

    const module = await Test.createTestingModule({
      imports: [
        MongooseModule.forRoot(uri),
        MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
      ],
      providers: [UserService],
    }).compile();

    service = module.get<UserService>(UserService);
  });

  afterAll(async () => {
    await mongod.stop();
  });

  it('should create a user', async () => {
    const user = await service.create({
      email: 'test@example.com',
      passwordHash: 'hashed',
    });
    expect(user.email).toBe('test@example.com');
  });
});

Conclusion

NestJS et MongoDB via Mongoose forment une combinaison puissante pour les applications Node.js modernes. La maîtrise des schémas typés, des pipelines d'agrégation et des transactions permet de construire des backends performants et maintenables. Les questions d'entretien testent la compréhension des compromis entre modélisation relationnelle et orientée document, ainsi que les techniques d'optimisation des performances pour les applications à forte charge.

Défi du jour

Tu saurais repérer le bug en Node.js / NestJS ?

Un vrai bout de code, un bug caché, une tentative par jour. Sans compte pour essayer.

Anthony Fillion-Maillet

Écrit par

Anthony Fillion-Maillet

Fondateur de SharpSkill

Développeur fullstack depuis plus de 10 ans. Il dirige SharpSkill et répond de tout ce qui y est publié.

Mis à jour le 28 août 2026

Tags

#nestjs
#mongodb
#mongoose
#nodejs
#backend

Partager

Articles similaires