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.

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 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.
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:
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 {}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:
{
"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.
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:
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;
}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:
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.
Bereit für deine Node.js / NestJS-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
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:
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:
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();
}
}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:
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);
});
}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.
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.
Bereit für deine Node.js / NestJS-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
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
synchronizein 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
Fang an zu üben!
Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.
Teilen
Verwandte Artikel

Node.js 24 im Jahr 2026: URLPattern, Permission Model und Interviewfragen
Node.js 24 LTS bringt ein stabiles Permission Model, globale URLPattern-API, explizites Ressourcenmanagement mit using/await using und V8 13.6. Ein detaillierter Einblick in die Funktionen, die für Produktion und Vorstellungsgespräche relevant sind.

Microservices mit NestJS in 2026: Architektur, gRPC und Interview-Fragen
Ein umfassender Leitfaden zur Entwicklung von Microservices mit NestJS, einschließlich gRPC-Integration, Kommunikationsmustern und typischen Fragen für technische Vorstellungsgespräche.

Node.js Performance: Event Loop, Clustering und Optimierung in 2026
Leistungsoptimierung von Node.js durch Event-Loop-Management, Clustering-Strategien und Worker Threads. Praxisnahe Muster für hochperformante Node.js-Anwendungen in 2026.