NestJS en TypeORM in 2026: Migraties, Relaties en Interviewvragen

NestJS en TypeORM vormen een beproefd duo voor schaalbare backend-applicaties. Dit artikel behandelt configuratie, migraties, relaties, transacties en interviewvragen.

NestJS en TypeORM in 2026: Migraties, Relaties en Interviewvragen

NestJS en TypeORM vormen al jaren een beproefd duo voor het bouwen van schaalbare backend-applicaties met Node.js. In 2026 blijft deze combinatie relevant, mede dankzij de stabiliteit van het decorator-gebaseerde entity-systeem van TypeORM en de modulaire architectuur van NestJS. Toch blijkt in de praktijk dat veel ontwikkelteams worstelen met databasemigraties, het correct opzetten van relaties en het vermijden van veelvoorkomende valkuilen in productieomgevingen.

Dit artikel behandelt de volledige integratie van TypeORM in een NestJS-project: van configuratie en migratiebeheer tot het modelleren van relaties en het uitvoeren van transacties. Aan het einde staan veelgestelde interviewvragen die regelmatig terugkomen bij technische sollicitatiegesprekken voor backend-posities.

Vereisten

De codevoorbeelden in dit artikel zijn gebaseerd op NestJS 11 met TypeORM en PostgreSQL. Alle configuratie maakt gebruik van omgevingsvariabelen via @nestjs/config, wat de aanbevolen werkwijze is voor productieomgevingen.

TypeORM configureren in een NestJS-project

De configuratie van TypeORM kent twee kanten: een standalone DataSource voor de CLI (migraties genereren en uitvoeren) en een module-gebaseerde configuratie binnen NestJS zelf. Beide moeten dezelfde databasecredentials gebruiken, maar dienen een ander doel.

De standalone configuratie wordt gebruikt door de TypeORM CLI en vereist expliciete paden naar entities en migraties:

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

Binnen de NestJS-applicatie wordt TypeORM geregistreerd via TypeOrmModule.forRootAsync, waardoor de ConfigService beschikbaar is voor het ophalen van omgevingsvariabelen:

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

De instelling synchronize: false verdient bijzondere aandacht. Wanneer synchronize op true staat, past TypeORM het databaseschema automatisch aan bij elke applicatiestart. Dit is handig tijdens lokale ontwikkeling, maar kan catastrofale gevolgen hebben in productie: kolommen kunnen onbedoeld verwijderd worden, data kan verloren gaan. Professionele teams gebruiken daarom altijd migraties.

Migraties: schemawijzigingen beheren

Migraties vormen het versiebeheersysteem voor het databaseschema. Elke migratie bevat een up-methode (wijziging toepassen) en een down-methode (wijziging terugdraaien). TypeORM kan migraties automatisch genereren door het huidige schema te vergelijken met de entity-definities.

De volgende npm-scripts maken het werken met migraties overzichtelijk:

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

De typische workflow ziet er als volgt uit:

  1. Pas een entity aan (voeg een kolom toe, wijzig een type, voeg een relatie toe).
  2. Genereer een migratie met npm run migration:generate --name=AddPhoneToUser.
  3. Controleer het gegenereerde migratiebestand in src/migrations/.
  4. Voer de migratie uit met npm run migration:run.
  5. Draai indien nodig terug met npm run migration:revert.
Controleer gegenereerde migraties

Een hernoemde kolom genereert een DROP + ADD in plaats van een ALTER RENAME. Dit vernietigt bestaande data. Controleer altijd de SQL-instructies in de up()-methode en pas ze indien nodig handmatig aan.

Het is belangrijk om gegenereerde migraties altijd handmatig te controleren voordat ze worden uitgevoerd. TypeORM genereert soms onnodige ALTER-statements of mist subtiele wijzigingen. In CI/CD-pipelines worden migraties doorgaans automatisch uitgevoerd als onderdeel van het deploymentproces, vlak voordat de nieuwe applicatieversie wordt gestart.

Entities en relaties modelleren

TypeORM gebruikt decorators om databasetabellen en hun onderlinge relaties te definiëren. De drie meest voorkomende relatietypen zijn OneToMany/ManyToOne, OneToOne en ManyToMany.

OneToMany en ManyToOne

Een klassiek voorbeeld is de relatie tussen gebruikers en bestellingen: één gebruiker heeft meerdere bestellingen, elke bestelling hoort bij precies één gebruiker.

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

De @OneToMany-decorator op de User-entity creëert geen kolom in de users-tabel. De daadwerkelijke foreign key leeft aan de "many"-kant van de relatie, in dit geval de Order-entity:

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

Het expliciet benoemen van de foreign key-kolom via @JoinColumn is geen vereiste, maar wel een goede gewoonte. Zonder deze decorator genereert TypeORM een kolomnaam als userId, wat kan afwijken van de naamgevingsconventies van het team of de bestaande database.

De optie onDelete: 'CASCADE' zorgt ervoor dat bij het verwijderen van een gebruiker alle bijbehorende bestellingen automatisch worden verwijderd door de database. Alternatieven zijn SET NULL (de foreign key wordt null) en RESTRICT (verwijdering wordt geblokkeerd zolang er gerelateerde records bestaan).

ManyToMany

Voor relaties waarbij records aan beide kanten meerdere tegenhangers kunnen hebben, zoals producten en categorieën, wordt een tussentabel (junction table) gebruikt:

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[];
}

De @JoinTable-decorator mag slechts aan één kant van de relatie staan en definieert welke entity de "eigenaar" is van de tussentabel. De Category-entity bevat dan alleen een @ManyToMany-decorator zonder @JoinTable.

Klaar om je Node.js / NestJS gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Repository-patroon en feature-modules

NestJS moedigt aan om elke functionaliteit in een eigen module te isoleren. De TypeOrmModule.forFeature()-methode registreert entities op moduleniveau, waardoor de bijbehorende repository automatisch beschikbaar wordt via dependency injection:

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

In de service wordt de repository geïnjecteerd met @InjectRepository. TypeORM biedt twee manieren om data op te vragen: de find-API voor eenvoudige queries en de QueryBuilder voor complexere scenario's:

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

De keuze tussen find en QueryBuilder hangt af van de complexiteit. Voor queries met filters op geneste relaties, subqueries of ruwe SQL-fragmenten is de QueryBuilder de betere optie. Voor standaard CRUD-operaties volstaat de find-API en levert deze leesbaarder code op.

Let op het gebruik van relations: ['user'] in de findByUser-methode. Zonder deze optie retourneert TypeORM de bestellingen zonder gebruikersgegevens, wat kan leiden tot undefined-fouten wanneer code later order.user.name probeert te benaderen.

Transacties: atomaire operaties garanderen

Wanneer meerdere databaseoperaties als één geheel moeten slagen of falen, zijn transacties onmisbaar. Een typisch voorbeeld is het aanmaken van een bestelling met bijbehorende orderregels:

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

Alle operaties binnen de callback van dataSource.transaction() delen dezelfde databasetransactie. Wanneer een van de operaties een fout gooit, wordt de volledige transactie teruggedraaid. Het is cruciaal om uitsluitend de manager te gebruiken die als parameter wordt meegegeven, en niet de reguliere repository. Het gebruik van de reguliere repository zou de operatie buiten de transactie uitvoeren.

QueryRunner vs transaction()

Gebruik DataSource.transaction() voor eenvoudige multi-step operaties. De QueryRunner biedt handmatige controle over commit/rollback-punten en savepoints, wat nuttig is bij partiële rollbacks in batchverwerking.

Veelgestelde interviewvragen over NestJS en TypeORM

Bij technische interviews voor backend-posities komen regelmatig vragen terug over de integratie van NestJS met TypeORM. Hieronder staan de meest voorkomende vragen met beknopte antwoorden.

Waarom moet synchronize op false staan in productie?

Met synchronize: true past TypeORM het databaseschema automatisch aan bij elke applicatiestart op basis van de entity-definities. In productie kan dit leiden tot onbedoeld dataverlies, bijvoorbeeld wanneer een kolom wordt hernoemd (TypeORM ziet dit als een verwijdering gevolgd door een nieuwe aanmaak). Migraties bieden controle, traceerbaarheid en de mogelijkheid om wijzigingen terug te draaien.

Wat is het verschil tussen @JoinColumn en @JoinTable?

@JoinColumn wordt gebruikt bij OneToOne- en ManyToOne-relaties en definieert welke kolom de foreign key bevat. @JoinTable wordt gebruikt bij ManyToMany-relaties en definieert de tussentabel. Beide decorators bepalen welke kant van de relatie de "eigenaar" is.

Wanneer gebruik je QueryBuilder in plaats van de find-API?

De find-API is geschikt voor eenvoudige queries met directe filters en eager loading. De QueryBuilder is noodzakelijk bij complexe joins, subqueries, aggregatiefuncties, HAVING-clausules of wanneer dynamische query-opbouw vereist is.

Hoe voorkom je het N+1-probleem met TypeORM?

Het N+1-probleem ontstaat wanneer voor elke record in een resultatenset een extra query wordt uitgevoerd om gerelateerde data op te halen. Dit wordt voorkomen door relaties expliciet mee te laden via de relations-optie in de find-methode of via leftJoinAndSelect in de QueryBuilder.

Wat gebeurt er als een transactie faalt?

Wanneer een fout optreedt binnen een dataSource.transaction()-callback, wordt de volledige transactie automatisch teruggedraaid (rollback). Alle wijzigingen die binnen de callback zijn gemaakt, worden ongedaan gemaakt. De fout wordt vervolgens doorgegooid naar de aanroepende code.

Hoe verschilt forRoot van forFeature bij TypeOrmModule?

forRoot (of forRootAsync) wordt één keer aangeroepen in de AppModule en configureert de databaseverbinding. forFeature wordt per feature-module aangeroepen en registreert specifieke entities, waardoor hun repositories beschikbaar worden voor dependency injection binnen die module.

Wat is het verschil tussen save en insert in TypeORM?

De save-methode voert een INSERT uit als het object geen primary key heeft, of een UPDATE als dat wel het geval is. Bovendien triggert save entity listeners en cascades. De insert-methode voert altijd een INSERT uit, is sneller voor bulk-operaties, maar triggert geen listeners of cascades.

Klaar om je Node.js / NestJS gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Conclusie

De combinatie van NestJS en TypeORM biedt een gestructureerde aanpak voor het bouwen van databasegedreven applicaties. De sleutels tot succes zijn:

  • Migraties gebruiken in plaats van automatische synchronisatie, met altijd handmatige controle van gegenereerde bestanden
  • Relaties bewust modelleren met de juiste decorators, wetende dat de foreign key op de @ManyToOne-kant leeft
  • Transacties inzetten waar atomiciteit vereist is, met consistent gebruik van de transactional manager
  • Het repository-patroon via feature-modules implementeren voor een modulaire en testbare codebase
  • De juiste keuze maken tussen find-API en QueryBuilder op basis van querycomplexiteit

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Delen

Gerelateerde artikelen