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 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.
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:
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:
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:
{
"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:
- Pas een entity aan (voeg een kolom toe, wijzig een type, voeg een relatie toe).
- Genereer een migratie met
npm run migration:generate --name=AddPhoneToUser. - Controleer het gegenereerde migratiebestand in
src/migrations/. - Voer de migratie uit met
npm run migration:run. - Draai indien nodig terug met
npm run migration:revert.
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.
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:
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:
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:
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:
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:
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.
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

Node.js 24 in 2026: URLPattern, Permission Model en Sollicitatievragen
Node.js 24 LTS brengt een stabiel Permission Model, globale URLPattern API, expliciet resourcebeheer met using/await using en V8 13.6. Een diepgaande analyse van de functies die relevant zijn voor productie en technische sollicitatiegesprekken.

Microservices met NestJS in 2026: Architectuur, gRPC en Sollicitatievragen
Een uitgebreide gids over het bouwen van schaalbare microservices met NestJS, inclusief gRPC-integratie, betrouwbaarheidspatronen en veelgestelde sollicitatievragen voor senior ontwikkelaars.

Node.js Performance: Event Loop, Clustering en Optimalisatie in 2026
Node.js-prestatieoptimalisatie via event loop management, clusteringstrategieën en worker threads. Praktische patronen voor high-throughput Node.js-applicaties in 2026.