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.

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.
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 :
# Installation de l'intégration Mongoose
npm install @nestjs/mongoose mongooseEnregistrement de la connexion dans le module racine :
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.
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.
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.
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.
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.
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.
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.
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;
}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.
// 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é.
// 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.
// 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.
// 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.
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.
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.

Écrit par
Anthony Fillion-MailletFondateur 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
Partager
Articles similaires

NestJS + Prisma : La stack moderne pour le backend Node.js
Guide complet pour créer une API backend moderne avec NestJS et Prisma. Configuration, modèles, services, transactions et bonnes pratiques expliquées.

NestJS : Créer une API REST complète
Guide complet pour créer une API REST professionnelle avec NestJS. Controllers, Services, Modules, validation avec class-validator et gestion des erreurs expliqués.

Questions d'entretien Node.js backend : Guide complet 2026
Les 25 questions d'entretien Node.js backend les plus fréquentes. Event loop, async/await, streams, clustering et performance expliqués avec des réponses détaillées.