# 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. - Published: 2026-06-19 - Updated: 2026-06-19 - Author: SharpSkill - Reading time: 5 min --- 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: ```typescript // src/config/typeorm.config.ts 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: ```typescript // src/app.module.ts 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('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: ```json // package.json (scripts section) { "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. ```typescript // src/entities/user.entity.ts 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: ```typescript // src/entities/order.entity.ts 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: ```typescript // src/entities/product.entity.ts 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`. ## 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: ```typescript // src/orders/orders.module.ts 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: ```typescript // src/orders/orders.service.ts 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, ) {} async findByUser(userId: string): Promise { 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 { // 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: ```typescript // src/orders/orders.service.ts (additional method) async createOrderWithItems( userId: string, items: { productId: string; quantity: number }[], dataSource: DataSource, ): Promise { 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. ## 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 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/nl/blog/node-nestjs/nestjs-typeorm-migrations-relations-best-practices