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.

Schéma d'architecture des migrations et relations NestJS et TypeORM

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.

Le jalon TypeORM 1.0

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.

src/config/typeorm.config.tstypescript
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 :

src/app.module.tstypescript
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 :

package.json (scripts section)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.

Relire chaque migration générée

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 :

src/entities/user.entity.tstypescript
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;
}
src/entities/order.entity.tstypescript
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 :

src/entities/product.entity.tstypescript
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.

src/orders/orders.module.tstypescript
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 {}
src/orders/orders.service.tstypescript
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.

src/orders/orders.service.ts (additional method)typescript
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.

QueryRunner ou 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 DataSource distinct pour le CLI TypeORM, indépendant de la configuration du module NestJS
  • Ne jamais activer synchronize en production ; s'appuyer sur des migrations générées et relues pour tous les changements de schéma
  • Placer @ManyToOne sur l'entité qui détient la clé étrangère et utiliser un nommage explicite via @JoinColumn pour 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 et QueryBuilder pour 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 le manager transactionnel
  • 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

#nestjs
#typeorm
#migrations
#database
#typescript
#backend

Partager

Articles similaires