NestJS und TypeORM 2026: Migrationen, Relationen und Interviewfragen

NestJS TypeORM Integration mit TypeORM 1.1 Migrationen, Entity-Relationen, Repository-Patterns und häufigen Interviewfragen für Backend-Entwickler.

NestJS und TypeORM 2026: Migrationen, Relationen und Interviewfragen

NestJS TypeORM bleibt eine der bewährtesten ORM-Integrationen für Enterprise-Node.js-Backends. Mit TypeORM Version 1.0 im Mai 2026, gefolgt von der 1.1.x-Patch-Serie, die Verbindungshandling und Relationspersistenz verbessert, sowie der abgeschlossenen ESM-Migration in NestJS 12, bietet der Stack eine stabile Grundlage für Produktionsanwendungen. Dieser Artikel behandelt die wesentlichen Patterns für Backend-Entwickler: Migrationen, Entity-Relationen, das Repository-Pattern und die zugehörigen Interviewfragen.

NestJS 12 und TypeORM 1.1

NestJS 12 liefert Core-Pakete als ESM und erfordert Node.js 20.19+ oder 22.12+. Das Paket @nestjs/typeorm v12 folgt diesem Ansatz. TypeORM 1.1.2 behebt Verbindungslecks in DataSource.initialize() und korrigiert die Persistenz von Many-to-Many-Junction-Rows, wenn Relationen nicht geladen sind.

TypeORM-Migrationen in NestJS 12 einrichten

Die erste Regel für Produktionsdatenbanken: niemals synchronize: true verwenden. Auto-Sync vergleicht Entity-Metadaten mit dem Live-Schema und wendet Änderungen direkt an, was bei Spaltenentfernungen, Typänderungen oder umbenannten Feldern zu Datenverlust führen kann. Migrationen bieten eine versionierte, überprüfbare und reversible Alternative.

TypeORM 1.x erfordert eine dedizierte DataSource-Konfigurationsdatei für die CLI, getrennt von der NestJS-Modulkonfiguration. Die CLI kann NestJS Dependency Injection nicht auflösen und benötigt daher einen eigenständigen Einstiegspunkt.

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

Die NestJS-Modulkonfiguration spiegelt diese Einstellungen wider, verwendet jedoch das TypeOrmModule.forRootAsync-Pattern mit Dependency Injection:

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

autoLoadEntities: true ersetzt das manuelle entities-Array durch automatische Registrierung jeder Entity, die über TypeOrmModule.forFeature() in Feature-Modulen importiert wird.

Migrations-Workflow: Generieren, Prüfen, Ausführen

TypeORM 1.x liefert typeorm-ts-node-commonjs als empfohlenen CLI-Runner, der den älteren ts-node ./node_modules/typeorm/cli-Ansatz ersetzt. Diese Scripts sollten in package.json hinzugefügt werden:

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

Der Generate-Befehl vergleicht Entity-Metadaten mit der Live-Datenbank und erzeugt SQL-Statements. Die Ausgabe sollte vor dem Commit immer geprüft werden. TypeORM erkennt Spaltenhinzufügungen, -entfernungen, Typänderungen, Index-Modifikationen und Fremdschlüsseländerungen, übersieht jedoch Daten-Backfills, Tabellenumbenennungen (die als Drop gefolgt von Create erscheinen) und alles, was Datenbankerweiterungen oder Trigger betrifft.

Generierte Migrationen prüfen

Eine umbenannte Spalte erzeugt ein DROP + ADD statt eines ALTER RENAME. Dies vernichtet vorhandene Daten. Die SQL-Anweisungen in der up()-Methode sollten immer geprüft und bei Bedarf manuell angepasst werden.

Entity-Relationen: OneToMany, ManyToOne, ManyToMany

TypeORM Entity-Relationen werden über Decorators direkt auf SQL-Fremdschlüssel abgebildet. Das Verständnis der drei Kern-Relationstypen und ihrer Optionen ist grundlegend für jedes NestJS-Datenbankmodul.

Ein User mit mehreren Order-Datensätzen zeigt das häufigste Pattern:

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

Die @ManyToOne-Seite besitzt den Fremdschlüssel. @JoinColumn ist bei @ManyToOne optional (TypeORM leitet es ab), aber explizite Benennung vermeidet Überraschungen bei Raw-Queries oder Migrations-Diffs.

Für Many-to-Many-Relationen erstellt TypeORM automatisch eine Junction-Tabelle:

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

@JoinTable muss auf genau einer Seite der Relation erscheinen. Es definiert, welche Entity die Junction-Tabelle besitzt und kontrolliert deren Namen.

Bereit für deine Node.js / NestJS-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Repository-Pattern und Custom Repositories

Das NestJS-Modul- und DI-System integriert TypeORM-Repositories über TypeOrmModule.forFeature(). Die Injektion des Standard-Repository<T> deckt Standard-CRUD-Operationen ab. Bei komplexen Queries kapseln Custom Repositories diese Logik.

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

Die find()-API behandelt unkomplizierte Queries. Der QueryBuilder behandelt Joins, Subqueries, Aggregationen und alles, was feingranulare SQL-Kontrolle erfordert. Beides im selben Service zu mischen ist vollkommen gültig: find() für einfache Lookups und QueryBuilder wenn die Query nicht über die Options-API ausgedrückt werden kann.

Transaktionsmanagement für Datenintegrität

Operationen, die mehrere Tabellen modifizieren, benötigen Transaktionen. TypeORM bietet zwei Ansätze: die DataSource.transaction()-Methode und die QueryRunner-API.

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

Die Callback-basierte transaction()-Methode committet automatisch bei Erfolg und rollt bei jedem geworfenen Fehler zurück. Jede Datenbankoperation innerhalb des Callbacks muss den bereitgestellten manager anstelle des injizierten Repositorys verwenden, andernfalls laufen diese Operationen außerhalb der Transaktion.

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 NestJS TypeORM Interviewfragen

Diese Fragen erscheinen regelmäßig in Backend-Entwickler-Interviews. Jede Antwort zielt auf die Tiefe, die in einer technischen Diskussion auf Senior-Level erwartet wird.

Warum sollte synchronize: true niemals in Produktion verwendet werden?

Auto-Sync wendet destruktive Schemaänderungen ohne Bestätigung an. Das Entfernen einer Spalte, das Ändern eines Typs oder das Entfernen eines Index geschieht sofort auf Live-Daten. Migrationen bieten Versionskontrolle, Code-Review und Rollback-Fähigkeit. Der einzig sichere Einsatz von synchronize ist lokale Entwicklung mit Wegwerfdaten.

Was ist der Unterschied zwischen eager und lazy Relationen?

Eager-Relationen werden automatisch mit jeder Query auf die Parent-Entity geladen. Lazy-Relationen geben ein Promise zurück und führen eine separate Query nur beim Zugriff aus. Eager Loading verursacht N+1-ähnlichen Overhead, wenn die verwandten Daten nicht benötigt werden. Lazy Loading verzögert die Kosten, kann aber unerwartete Queries tief im Call-Stack auslösen. Die explizite relations-Option in find() oder leftJoinAndSelect im QueryBuilder gibt volle Kontrolle darüber, was pro Query geladen wird.

Wie funktioniert cascade bei TypeORM-Relationen?

Das Setzen von cascade: true auf einer Relation bedeutet, dass das Speichern der Parent-Entity auch nicht gespeicherte Child-Entities persistiert. cascade: ['insert', 'update'] beschränkt das Verhalten auf bestimmte Operationen. Cascades operieren auf ORM-Ebene, nicht auf Datenbankebene. Für kaskadierende Löschungen auf Datenbankebene wird onDelete: 'CASCADE' auf dem @ManyToOne-Decorator verwendet, der einen ON DELETE CASCADE-Constraint in der Migration erzeugt.

Wann sollte ein Custom Repository anstelle des Standard-Repository<T> verwendet werden?

Custom Repositories kapseln komplexe Query-Logik, die nicht in Services gehört. Anzeichen für die Extraktion eines Custom Repositorys: dieselbe QueryBuilder-Kette erscheint in mehreren Services, die Query beinhaltet mehrere Joins oder Subqueries, oder die Query erfordert Raw SQL. Einfache find()-Aufrufe rechtfertigen kein Custom Repository.

Für weitere NestJS-Architekturfragen, siehe den vollständigen NestJS Guards und Interceptors Guide.

Bereit für deine Node.js / NestJS-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Sources

Wichtige Erkenntnisse für NestJS TypeORM Produktionsanwendungen

  • Eine separate DataSource-Konfigurationsdatei für die TypeORM-CLI verwenden, unabhängig von der NestJS-Modulkonfiguration
  • Niemals synchronize in Produktion aktivieren; stattdessen auf generierte und geprüfte Migrationen für alle Schemaänderungen setzen
  • @ManyToOne auf die Entity setzen, die den Fremdschlüssel besitzt, und explizite @JoinColumn-Benennung für Klarheit bei Raw-Queries und Migrations-Output verwenden
  • Zwischen find()-Options-API für einfache Queries und QueryBuilder für komplexe Joins, Aggregationen und bedingte Logik wählen
  • Multi-Tabellen-Mutationen in Transaktionen über DataSource.transaction() wrappen und sicherstellen, dass jede Operation innerhalb des Callbacks den transaktionalen manager verwendet
  • Mit TypeORM 1.1.x, das die API-Oberfläche stabilisiert, und NestJS 12, das die ESM-Migration abschließt, bleibt dieser Stack eine starke Wahl für Enterprise-Backends, die strukturierten, typsicheren Datenbankzugriff erfordern

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Tägliche Challenge

Findest du den Bug in Node.js / NestJS?

Ein echter Codeausschnitt, ein versteckter Bug, ein Versuch pro Tag. Zum Ausprobieren ohne Konto.

Anthony Fillion-Maillet

Geschrieben von

Anthony Fillion-Maillet

Gründer von SharpSkill

Seit über 10 Jahren Fullstack-Entwickler. Er leitet SharpSkill und verantwortet alles, was hier erscheint.

Aktualisiert am 21. Oktober 2026

Tags

#nestjs
#typeorm
#migrations
#database
#typescript
#backend

Teilen

Verwandte Artikel