NestJS et TypeORM en 2026 : migrations, relations et questions d'entretien
Maîtriser l'intégration NestJS TypeORM avec les migrations de TypeORM 1.0, les relations entre entités, le pattern repository et les questions d'entretien courantes pour les développeurs backend.

L'intégration NestJS TypeORM reste l'une des plus éprouvées pour les backends Node.js d'entreprise. Avec TypeORM qui atteint la version 1.0 en mai 2026 après presque une décennie de développement, et NestJS 11 qui livre Express v5 par défaut, cette stack est devenue une fondation stable pour les applications de production. Ce guide couvre les patterns essentiels que tout développeur backend doit maîtriser : les migrations, les relations entre entités, le pattern repository et les questions d'entretien qui les accompagnent.
TypeORM 1.0.0, publié le 19 mai 2026, modernise les prérequis de la plateforme vers ECMAScript 2023, supprime les API dépréciées et corrige l'ordre des migrations : les migrations en attente s'exécutent désormais avant la synchronisation du schéma lorsque les deux sont activées.
Configurer les migrations TypeORM dans NestJS 11
Première règle des bases de données de production : ne jamais utiliser synchronize: true. La synchronisation automatique compare les métadonnées des entités avec le schéma en place et applique les changements directement, ce qui expose à une perte de données lors de suppressions de colonnes, de changements de type ou de renommages de champs. Les migrations offrent une alternative versionnée, relisible et réversible.
TypeORM 1.0 exige un fichier de configuration DataSource dédié pour le CLI, distinct de la configuration du module NestJS. Le CLI ne peut pas résoudre l'injection de dépendances de NestJS, il lui faut donc un point d'entrée autonome.
import { DataSource } from 'typeorm';
import { config } from 'dotenv';
config(); // Load .env variables
export default new DataSource({
type: 'postgres',
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '5432'),
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
entities: ['src/**/*.entity.ts'],
migrations: ['src/migrations/*.ts'],
synchronize: false,
});La configuration du module NestJS reprend ces réglages mais utilise le pattern TypeOrmModule.forRootAsync avec injection de dépendances :
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'postgres',
host: config.get('DB_HOST'),
port: config.getOrThrow<number>('DB_PORT'),
username: config.get('DB_USER'),
password: config.get('DB_PASSWORD'),
database: config.get('DB_NAME'),
autoLoadEntities: true, // Auto-register entities from feature modules
synchronize: false, // Always false in production
}),
}),
],
})
export class AppModule {}autoLoadEntities: true remplace le tableau entities manuel en enregistrant automatiquement chaque entité importée via TypeOrmModule.forFeature() dans les modules de fonctionnalité.
Le flux de migration : générer, relire, exécuter
TypeORM 1.0 livre typeorm-ts-node-commonjs comme runner CLI recommandé, en remplacement de l'ancienne approche ts-node ./node_modules/typeorm/cli. Ajouter ces scripts au package.json :
{
"migration:generate": "typeorm-ts-node-commonjs migration:generate src/migrations/$npm_config_name -d src/config/typeorm.config.ts",
"migration:run": "typeorm-ts-node-commonjs migration:run -d src/config/typeorm.config.ts",
"migration:revert": "typeorm-ts-node-commonjs migration:revert -d src/config/typeorm.config.ts"
}La commande de génération compare les métadonnées des entités à la base de données en place et produit des instructions SQL. Toujours relire la sortie avant de la committer. TypeORM détecte les ajouts de colonnes, les suppressions, les changements de type, les modifications d'index et les changements de clé étrangère, mais il manque les backfills de données, les renommages de tables (qui apparaissent comme une suppression suivie d'une création) et tout ce qui touche aux extensions ou aux triggers de la base.
Une colonne renommée génère un DROP + ADD au lieu d'un ALTER RENAME. Cela détruit les données existantes. Toujours inspecter le SQL dans la méthode up() et l'ajuster manuellement si nécessaire.
Relations entre entités : OneToMany, ManyToOne, ManyToMany
Les relations entre entités TypeORM se traduisent directement en clés étrangères SQL grâce aux décorateurs. Comprendre les trois types de relations fondamentaux et leurs options est essentiel pour tout module base de données NestJS.
Un User avec plusieurs enregistrements Order illustre le pattern le plus courant :
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, CreateDateColumn } from 'typeorm';
import { Order } from './order.entity';
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
@Column()
name: string;
@OneToMany(() => Order, (order) => order.user)
orders: Order[]; // No DB column created here; relation lives on the Order side
@CreateDateColumn()
createdAt: Date;
}import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './user.entity';
@Entity('orders')
export class Order {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column('decimal', { precision: 10, scale: 2 })
total: number;
@Column({ default: 'pending' })
status: string;
@ManyToOne(() => User, (user) => user.orders, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' }) // Explicit FK column name
user: User;
}Le côté @ManyToOne détient la clé étrangère. @JoinColumn est optionnel sur @ManyToOne (TypeORM l'infère), mais un nommage explicite évite les surprises lorsque le nom de colonne compte pour des requêtes brutes ou des diffs de migration.
Pour les relations many-to-many, TypeORM crée automatiquement une table de jonction :
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Category } from './category.entity';
@Entity('products')
export class Product {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
@Column('decimal', { precision: 10, scale: 2 })
price: number;
@ManyToMany(() => Category, (category) => category.products)
@JoinTable({ name: 'product_categories' }) // Owns the junction table
categories: Category[];
}@JoinTable doit apparaître sur un seul côté de la relation. Il définit quelle entité détient la table de jonction et contrôle son nom.
Prêt à réussir tes entretiens Node.js / NestJS ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Le pattern repository et les repositories personnalisés
Le système de modules et d'injection de dépendances de NestJS intègre les repositories TypeORM via TypeOrmModule.forFeature(). Injecter le Repository<T> par défaut couvre les opérations CRUD standard. Lorsque les requêtes se complexifient, les repositories personnalisés encapsulent cette logique.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Order } from '../entities/order.entity';
import { OrdersService } from './orders.service';
import { OrdersController } from './orders.controller';
@Module({
imports: [TypeOrmModule.forFeature([Order])],
providers: [OrdersService],
controllers: [OrdersController],
})
export class OrdersModule {}import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Order } from '../entities/order.entity';
@Injectable()
export class OrdersService {
constructor(
@InjectRepository(Order)
private readonly orderRepo: Repository<Order>,
) {}
async findByUser(userId: string): Promise<Order[]> {
return this.orderRepo.find({
where: { user: { id: userId } },
relations: ['user'], // Eager-load the user relation
order: { createdAt: 'DESC' }, // Most recent first
});
}
async findWithFilters(status: string, minTotal: number): Promise<Order[]> {
// QueryBuilder for complex queries
return this.orderRepo
.createQueryBuilder('order')
.leftJoinAndSelect('order.user', 'user')
.where('order.status = :status', { status })
.andWhere('order.total >= :minTotal', { minTotal })
.orderBy('order.total', 'DESC')
.getMany();
}
}L'API find() gère les requêtes simples. Le QueryBuilder gère les jointures, les sous-requêtes, les agrégations et tout ce qui exige un contrôle fin du SQL. Mélanger les deux dans un même service est parfaitement valide : utiliser find() pour les recherches simples et QueryBuilder lorsque la requête ne peut pas s'exprimer via l'API d'options.
Gestion des transactions pour l'intégrité des données
Les opérations qui modifient plusieurs tables ont besoin de transactions. TypeORM propose deux approches : la méthode DataSource.transaction() et l'API QueryRunner.
async createOrderWithItems(
userId: string,
items: { productId: string; quantity: number }[],
dataSource: DataSource,
): Promise<Order> {
return dataSource.transaction(async (manager) => {
// All operations use the transactional manager
const order = manager.create(Order, {
user: { id: userId },
total: 0,
status: 'pending',
});
const savedOrder = await manager.save(order);
let total = 0;
for (const item of items) {
const product = await manager.findOneByOrFail(Product, { id: item.productId });
total += product.price * item.quantity;
await manager.save(OrderItem, {
order: savedOrder,
product,
quantity: item.quantity,
unitPrice: product.price,
});
}
savedOrder.total = total;
return manager.save(savedOrder);
});
}La méthode transaction() à base de callback committe automatiquement en cas de succès et effectue un rollback dès qu'une erreur est levée. Chaque opération de base de données à l'intérieur du callback doit utiliser le manager fourni plutôt que le repository injecté, sinon ces opérations s'exécutent en dehors de la transaction.
Utiliser DataSource.transaction() pour les opérations multi-étapes simples. Passer à QueryRunner lorsqu'un contrôle manuel des points de commit/rollback ou des savepoints est nécessaire, comme les rollbacks partiels dans un traitement par lots.
Questions d'entretien courantes sur NestJS TypeORM
Ces questions reviennent fréquemment dans les entretiens de développeurs backend. Chaque réponse vise la profondeur attendue dans une discussion technique de niveau senior.
Pourquoi ne faut-il jamais utiliser synchronize: true en production ?
La synchronisation automatique applique des changements de schéma destructeurs sans confirmation. Supprimer une colonne, changer un type ou retirer un index se produit immédiatement sur des données en production. Les migrations apportent le versionnage, la relecture de code et la capacité de rollback. Le seul usage sûr de synchronize est le développement local avec des données jetables.
Quelle est la différence entre les relations eager et lazy ?
Les relations eager se chargent automatiquement à chaque requête sur l'entité parente. Les relations lazy renvoient une Promise et exécutent une requête séparée uniquement lors de l'accès. Le chargement eager engendre une surcharge de type N+1 lorsque les données liées ne sont pas nécessaires. Le chargement lazy diffère le coût mais peut déclencher des requêtes inattendues au fond de la pile d'appels. L'option explicite relations dans find() ou leftJoinAndSelect dans QueryBuilder donne un contrôle total sur ce qui est chargé par requête.
Comment fonctionne cascade dans les relations TypeORM ?
Définir cascade: true sur une relation signifie qu'enregistrer l'entité parente persiste aussi les entités enfants non enregistrées. cascade: ['insert', 'update'] limite le comportement à des opérations spécifiques. Les cascades opèrent au niveau de l'ORM, pas au niveau de la base de données. Pour des suppressions en cascade au niveau base de données, utiliser onDelete: 'CASCADE' sur le décorateur @ManyToOne, qui génère une contrainte ON DELETE CASCADE dans la migration.
Quand faut-il utiliser un repository personnalisé plutôt que le Repository<T> par défaut ?
Les repositories personnalisés encapsulent une logique de requête complexe qui n'a pas sa place dans les services. Les signes pour extraire un repository personnalisé : la même chaîne QueryBuilder apparaît dans plusieurs services, la requête implique plusieurs jointures ou sous-requêtes, ou la requête exige du SQL brut. De simples appels find() ne justifient pas un repository personnalisé.
Pour davantage de questions sur l'architecture NestJS, voir le guide complet sur les guards et interceptors NestJS.
Prêt à réussir tes entretiens Node.js / NestJS ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Conclusion
- Utiliser un fichier de configuration
DataSourcedistinct pour le CLI TypeORM, indépendant de la configuration du module NestJS - Ne jamais activer
synchronizeen production ; s'appuyer sur des migrations générées et relues pour tous les changements de schéma - Placer
@ManyToOnesur l'entité qui détient la clé étrangère et utiliser un nommage explicite via@JoinColumnpour plus de clarté dans les requêtes brutes et la sortie des migrations - Choisir entre l'API d'options
find()pour les requêtes simples etQueryBuilderpour les jointures complexes, les agrégations et la logique conditionnelle - Encapsuler les mutations multi-tables dans des transactions via
DataSource.transaction(), et veiller à ce que chaque opération dans le callback utilise lemanagertransactionnel - Avec TypeORM 1.0 qui stabilise la surface de son API et NestJS 11 qui mûrit son architecture de modules, cette stack reste un choix solide pour les backends d'entreprise qui exigent un accès aux données structuré et typé
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
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.

Microservices NestJS en 2026 : Architecture gRPC, Streaming et Questions d'Entretien Technique
Guide pratique sur l'architecture microservices NestJS avec gRPC : couches de transport, Protocol Buffers, patterns de streaming et questions d'entretien pour développeurs backend en 2026.