# NestJS und TypeORM 2026: Migrationen, Relationen und Interviewfragen > NestJS und TypeORM gehören 2026 zu den etabliertesten Kombinationen im Node.js-Backend-Bereich. Dieser Artikel behandelt Migrationen, Relationen, Transaktionen und Interviewfragen. - Published: 2026-06-19 - Updated: 2026-06-19 - Author: SharpSkill - Reading time: 5 min --- NestJS und TypeORM gehören 2026 zu den etabliertesten Kombinationen im Node.js-Backend-Bereich. Während NestJS mit seiner modularen Architektur und Dependency Injection überzeugt, liefert TypeORM das nötige Werkzeug für typsichere Datenbankoperationen. Doch gerade bei Migrationen, komplexen Relationen und dem Zusammenspiel beider Technologien treten in der Praxis Fragen auf, die auch in technischen Interviews regelmäßig gestellt werden. Dieser Artikel behandelt die wichtigsten Konzepte rund um TypeORM-Migrationen in NestJS-Projekten, beleuchtet die verschiedenen Relationstypen anhand konkreter Codebeispiele und schließt mit praxisnahen Interviewfragen ab, die das Verständnis vertiefen. > **TypeORM DataSource-API** > > TypeORM hat die Konfigurations-API grundlegend überarbeitet. Projekte, die noch auf `ormconfig.json` oder `createConnection` setzen, sollten auf die `DataSource`-API migrieren. Alle Beispiele in diesem Artikel verwenden die aktuelle API. ## TypeORM-Konfiguration in NestJS einrichten Der erste Schritt besteht darin, eine eigenständige DataSource-Konfiguration anzulegen. Diese Datei wird von der TypeORM-CLI für Migrationen verwendet. Die Trennung sorgt dafür, dass die CLI unabhängig vom NestJS-Kontext arbeiten kann. ```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, }); ``` Die Eigenschaft `synchronize: false` verdient besondere Beachtung. Im Entwicklungsmodus mag es verlockend sein, `synchronize: true` zu setzen, damit TypeORM das Datenbankschema automatisch anpasst. In Produktionsumgebungen führt das jedoch zu Datenverlust und unvorhersehbarem Verhalten. Stattdessen kommen Migrationen zum Einsatz. Im `AppModule` wird TypeORM über die asynchrone Konfiguration eingebunden, wobei der `ConfigService` die Umgebungsvariablen bereitstellt: ```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 {} ``` Der Unterschied zwischen `config.get()` und `config.getOrThrow()` ist subtil, aber relevant: `getOrThrow()` wirft eine Exception, wenn die Variable fehlt. Für kritische Konfigurationswerte wie den Datenbankport stellt das sicher, dass Fehlkonfigurationen sofort auffallen, statt stumm Standardwerte zu verwenden. ## Migrationen: Erstellen, Ausführen und Zurücksetzen Migrationen bilden das Rückgrat jeder professionellen Datenbankstrategie. Sie dokumentieren Schemaänderungen versioniert und reproduzierbar. Folgende npm-Scripts vereinfachen die Arbeit mit der TypeORM-CLI: ```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" } ``` Der Workflow sieht in der Praxis folgendermaßen aus: Zuerst werden die Entities angepasst, dann generiert `npm run migration:generate --name=AddUserTable` automatisch eine Migrationsdatei, die den Unterschied zwischen dem aktuellen Schema und den Entity-Definitionen abbildet. Mit `npm run migration:run` wird die Migration angewendet, und `npm run migration:revert` macht die letzte Migration rückgängig. Ein häufiger Fehler in Projekten ist das manuelle Schreiben von Migrationen, obwohl `migration:generate` den Abgleich automatisch übernimmt. Manuelle Migrationen sind nur dann sinnvoll, wenn Daten transformiert werden müssen, etwa beim Umbenennen einer Spalte mit anschließender Datenmigration. > **Generierte Migrationen prüfen** > > Eine umbenannte Spalte erzeugt ein DROP + ADD statt eines ALTER RENAME. Das vernichtet vorhandene Daten. Die SQL-Anweisungen in der `up()`-Methode sollten immer überprüft und bei Bedarf manuell angepasst werden. ## Entities und Relationen modellieren TypeORM setzt auf Decorators, um Datenbankstrukturen direkt in TypeScript-Klassen abzubilden. Die drei wichtigsten Relationstypen sind OneToMany/ManyToOne, ManyToMany und OneToOne. ### OneToMany und ManyToOne Die klassische Beziehung zwischen Nutzern und Bestellungen zeigt das Zusammenspiel von `@OneToMany` und `@ManyToOne`: ```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; } ``` ```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; } ``` Entscheidend ist hier: Die `@OneToMany`-Seite erzeugt keine Spalte in der Datenbank. Der Fremdschlüssel liegt immer auf der `@ManyToOne`-Seite. Der `@JoinColumn`-Decorator bestimmt den Spaltennamen des Fremdschlüssels explizit, was die Lesbarkeit der Datenbank erhöht und Namenskonflikte vermeidet. Die Option `onDelete: 'CASCADE'` sorgt dafür, dass Bestellungen automatisch gelöscht werden, wenn der zugehörige Nutzer entfernt wird. ### ManyToMany Für Produkte und Kategorien eignet sich eine ManyToMany-Beziehung. TypeORM erstellt dabei automatisch eine Zwischentabelle: ```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[]; } ``` Der `@JoinTable`-Decorator markiert die besitzende Seite der Relation. Nur eine Seite darf diesen Decorator tragen. Soll die Zwischentabelle zusätzliche Spalten enthalten (etwa eine Sortierreihenfolge), muss stattdessen eine eigene Entity mit zwei `@ManyToOne`-Relationen erstellt werden. ## Repository-Pattern und Feature-Module NestJS fördert die Modularisierung durch Feature-Module. Jedes Modul registriert seine Entities über `TypeOrmModule.forFeature()` und erhält damit Zugriff auf die entsprechenden Repositories: ```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 {} ``` Im Service wird das Repository per `@InjectRepository` injiziert. Für einfache Abfragen reicht die Repository-API, während der QueryBuilder bei komplexeren Szenarien zum Einsatz kommt: ```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(); } } ``` Die Methode `findByUser` zeigt, wie Relationen über verschachtelte Where-Bedingungen gefiltert werden. Das `relations`-Array steuert, welche Verknüpfungen geladen werden. Ohne diese Angabe liefert TypeORM nur die Bestellungen ohne Nutzerdaten. Der QueryBuilder in `findWithFilters` bietet mehr Kontrolle: `leftJoinAndSelect` lädt die Relation und macht sie in den Ergebnissen verfügbar, während parametrisierte Queries SQL-Injection zuverlässig verhindern. ## Transaktionen richtig einsetzen Wenn mehrere Datenbankoperationen atomar ablaufen müssen, sind Transaktionen unverzichtbar. TypeORM bietet hierfür die `DataSource.transaction()`-Methode: ```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); }); } ``` Innerhalb der Transaktion wird ausschließlich der `manager` für alle Operationen verwendet. Würde stattdessen das reguläre Repository genutzt, liefen diese Operationen außerhalb der Transaktion und ein Fehler bei einem späteren Schritt könnte zu inkonsistenten Daten führen. Bei einem Fehler innerhalb des Callbacks wird die gesamte Transaktion automatisch zurückgerollt. > **QueryRunner vs transaction()** > > `DataSource.transaction()` eignet sich für unkomplizierte Multi-Step-Operationen. Der `QueryRunner` bietet manuelle Kontrolle über Commit/Rollback-Punkte und Savepoints, etwa bei partiellen Rollbacks in Batch-Verarbeitungen. ## Häufige Interviewfragen zu NestJS und TypeORM Technische Interviews zu NestJS und TypeORM decken typischerweise drei Bereiche ab: Architekturverständnis, praktische Problemlösung und Fehlervermeidung. **Warum sollte `synchronize` in Produktion deaktiviert sein?** Die Option `synchronize: true` gleicht das Datenbankschema bei jedem Anwendungsstart automatisch mit den Entity-Definitionen ab. Das kann Spalten löschen, Datentypen ändern oder Tabellen entfernen, ohne dass dies nachvollziehbar dokumentiert ist. In Produktionsumgebungen führt das zu Datenverlust. Migrationen bieten stattdessen versionierte, reproduzierbare und rückgängig machbare Schemaänderungen. **Was ist der Unterschied zwischen `migration:generate` und `migration:create`?** Der Befehl `migration:generate` vergleicht die aktuellen Entity-Definitionen mit dem Datenbankschema und erzeugt automatisch die notwendigen SQL-Statements. `migration:create` hingegen erstellt eine leere Migrationsdatei, in die SQL-Befehle manuell geschrieben werden. Letzteres ist nützlich für Datenmigrationen oder Seed-Operationen. **Auf welcher Seite liegt der Fremdschlüssel bei OneToMany/ManyToOne?** Der Fremdschlüssel befindet sich immer auf der `@ManyToOne`-Seite. Die `@OneToMany`-Seite erzeugt keine Spalte in der Datenbank und dient lediglich dazu, die Rückbeziehung in TypeScript abzubilden. **Wann wird der QueryBuilder statt der Repository-API verwendet?** Die Repository-API (`find`, `findOne`, `save`) eignet sich für einfache CRUD-Operationen und Abfragen mit direkten Filterbedingungen. Der QueryBuilder kommt zum Einsatz, wenn komplexe Joins, Subqueries, aggregierte Funktionen oder dynamisch zusammengesetzte Abfragen benötigt werden. **Wie werden Circular Dependencies zwischen Modulen aufgelöst?** NestJS bietet `forwardRef()` für zirkuläre Abhängigkeiten zwischen Modulen. Allerdings deutet deren Einsatz oft auf ein Architekturproblem hin. Besser ist es, gemeinsam genutzte Logik in ein separates Modul auszulagern. **Was passiert, wenn innerhalb einer Transaktion das reguläre Repository statt des Managers verwendet wird?** Operationen über das reguläre Repository laufen außerhalb der Transaktion. Das bedeutet, diese Änderungen werden sofort committed und können im Fehlerfall nicht zurückgerollt werden. Das führt zu inkonsistenten Datenbeständen. ## Fazit Die Kombination aus NestJS und TypeORM bietet ein ausgereiftes Ökosystem für typsichere Backend-Entwicklung mit Node.js. Die zentralen Erkenntnisse lassen sich wie folgt zusammenfassen: - **Migrationen** ersetzen `synchronize` in jeder Umgebung jenseits lokaler Entwicklung - **Relationen** erfordern ein klares Verständnis davon, welche Seite den Fremdschlüssel besitzt - **Transaktionen** müssen konsequent über den Manager laufen, nicht über reguläre Repositories - Der **QueryBuilder** ergänzt die Repository-API dort, wo einfache Methoden an ihre Grenzen stoßen - Für technische Interviews zählt nicht nur die Syntax, sondern vor allem die Begründung hinter Architekturentscheidungen --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/de/blog/node-nestjs/nestjs-typeorm-migrations-relations-best-practices