NestJS i TypeORM w 2026 roku: migracje, relacje i pytania rekrutacyjne

NestJS w polaczeniu z TypeORM to jeden z najpopularniejszych stosow backendowych w ekosystemie Node.js. Artykul omawia migracje, relacje, transakcje i typowe pytania rekrutacyjne.

NestJS i TypeORM w 2026 roku: migracje, relacje i pytania rekrutacyjne

NestJS i TypeORM w 2026 roku stanowia jeden z najbardziej dojrzalych tandemow w swiecie backendu opartego na Node.js. NestJS zapewnia modularna architekture i wbudowany system wstrzykiwania zaleznosci, podczas gdy TypeORM dostarcza narzedzia do typobezpiecznych operacji bazodanowych. Jednak w codziennej praktyce projektowej — szczegolnie w obszarze migracji, zlozonych relacji i wspoldzialania obu technologii — pojawiaja sie pytania, ktore regularnie padaja takze na rozmowach kwalifikacyjnych.

Niniejszy artykul omawia kluczowe koncepcje zwiazane z migracjami TypeORM w projektach NestJS, przedstawia rozne typy relacji na podstawie konkretnych przykladow kodu oraz zamyka sie zestawem praktycznych pytan rekrutacyjnych poglebiajacych zrozumienie tematu.

TypeORM DataSource API

TypeORM gruntownie przebudowal swoje API konfiguracyjne. Projekty nadal korzystajace z ormconfig.json lub createConnection powinny przejsc na DataSource API. Wszystkie przyklady w tym artykule wykorzystuja aktualna wersje API.

Konfiguracja TypeORM w NestJS

Pierwszym krokiem jest utworzenie samodzielnej konfiguracji DataSource. Plik ten jest wykorzystywany przez CLI TypeORM do obslugi migracji. Oddzielenie konfiguracji pozwala CLI dzialac niezaleznie od kontekstu NestJS.

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

Wlasciwosc synchronize: false zasluguje na szczegolna uwage. W trybie deweloperskim moze byc kuszace ustawienie synchronize: true, aby TypeORM automatycznie aktualizowal schemat bazy danych. W srodowiskach produkcyjnych prowadzi to jednak do utraty danych i nieprzewidywalnego zachowania. Zamiast tego stosuje sie migracje.

W AppModule TypeORM jest integrowany za pomoca konfiguracji asynchronicznej, gdzie ConfigService dostarcza zmienne srodowiskowe:

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

Roznica miedzy config.get() a config.getOrThrow() jest subtelna, ale istotna: getOrThrow() rzuca wyjatek w przypadku braku zmiennej. Dla krytycznych wartosci konfiguracyjnych, takich jak port bazy danych, gwarantuje to natychmiastowe wykrycie bledow konfiguracji zamiast cichego uzywania wartosci domyslnych.

Migracje: tworzenie, uruchamianie i cofanie

Migracje stanowia fundament kazdej profesjonalnej strategii zarzadzania baza danych. Dokumentuja zmiany schematu w sposob wersjonowany i odtwarzalny. Ponizsze skrypty npm ulatwiaja prace z CLI TypeORM:

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

Praktyczny workflow wyglada nastepujaco: najpierw modyfikowane sa encje, nastepnie npm run migration:generate --name=AddUserTable automatycznie generuje plik migracji odzwierciedlajacy roznice miedzy aktualnym schematem a definicjami encji. Komenda npm run migration:run aplikuje migracje, a npm run migration:revert cofa ostatnia zastosowana migracje.

Czestym bledem w projektach jest reczne pisanie migracji, podczas gdy migration:generate automatycznie przeprowadza porownanie. Reczne migracje maja sens jedynie wtedy, gdy konieczna jest transformacja danych — na przyklad przy zmianie nazwy kolumny z jednoczesna migracja danych.

Weryfikacja wygenerowanych migracji

Zmiana nazwy kolumny generuje operacje DROP + ADD zamiast ALTER RENAME. To powoduje utrate istniejacych danych. Instrukcje SQL w metodzie up() powinny byc zawsze sprawdzane i w razie potrzeby recznie korygowane.

Modelowanie encji i relacji

TypeORM wykorzystuje dekoratory do mapowania struktur bazodanowych bezposrednio na klasy TypeScript. Trzy najwazniejsze typy relacji to OneToMany/ManyToOne, ManyToMany i OneToOne.

OneToMany i ManyToOne

Klasyczna relacja miedzy uzytkownikami a zamowieniami ilustruje wspoldzialanie @OneToMany i @ManyToOne:

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

Kluczowa kwestia: strona @OneToMany nie tworzy zadnej kolumny w bazie danych. Klucz obcy znajduje sie zawsze po stronie @ManyToOne. Dekorator @JoinColumn jawnie okresla nazwe kolumny klucza obcego, co poprawia czytelnosc bazy danych i zapobiega konfliktom nazw. Opcja onDelete: 'CASCADE' sprawia, ze zamowienia sa automatycznie usuwane po usunieciu powiazanego uzytkownika.

ManyToMany

Dla produktow i kategorii odpowiednia jest relacja ManyToMany. TypeORM automatycznie tworzy tabele posrednia:

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

Dekorator @JoinTable oznacza strone bedaca wlascicielem relacji. Tylko jedna strona moze posiadac ten dekorator. Jesli tabela posrednia ma zawierac dodatkowe kolumny (np. kolejnosc sortowania), nalezy zamiast tego utworzyc osobna encje z dwoma relacjami @ManyToOne.

Gotowy na rozmowy o Node.js / NestJS?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Wzorzec Repository i moduly funkcjonalne

NestJS promuje modularyzacje poprzez moduly funkcjonalne. Kazdy modul rejestruje swoje encje za pomoca TypeOrmModule.forFeature() i uzyskuje dostep do odpowiednich repozytoriow:

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

W serwisie repozytorium jest wstrzykiwane za pomoca @InjectRepository. Do prostych zapytan wystarczy Repository API, natomiast QueryBuilder jest stosowany w bardziej zlozonych scenariuszach:

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

Metoda findByUser pokazuje, jak filtrowac relacje za pomoca zagniezdonych warunkow Where. Tablica relations steruje ladowaniem powiazanych encji. Bez tej deklaracji TypeORM zwroci jedynie zamowienia bez danych uzytkownika.

QueryBuilder w findWithFilters oferuje wieksza kontrole: leftJoinAndSelect laduje relacje i udostepnia ja w wynikach, a parametryzowane zapytania skutecznie zapobiegaja atakom SQL injection.

Prawidlowe stosowanie transakcji

Gdy kilka operacji bazodanowych musi zostac wykonanych atomowo, transakcje sa niezbedne. TypeORM oferuje w tym celu metode DataSource.transaction():

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

Wewnatrz transakcji wylacznie manager jest uzywany do wszystkich operacji. Gdyby zamiast tego wykorzystano standardowe repozytorium, operacje te odbywaloby sie poza transakcja, a blad na pozniejszym etapie moglby prowadzic do niespojnych danych. W przypadku bledu wewnatrz callbacka cala transakcja jest automatycznie wycofywana.

QueryRunner vs transaction()

DataSource.transaction() sprawdza sie w prostych operacjach wielokrokowych. QueryRunner oferuje reczna kontrole nad punktami commit/rollback oraz savepointami — przydatna przy czesciowych wycofaniach w przetwarzaniu wsadowym.

Najczesciej zadawane pytania rekrutacyjne dotyczace NestJS i TypeORM

Rozmowy techniczne dotyczace NestJS i TypeORM obejmuja zwykle trzy obszary: zrozumienie architektury, praktyczne rozwiazywanie problemow i unikanie typowych bledow.

Dlaczego synchronize powinno byc wylaczone w produkcji?

Opcja synchronize: true automatycznie synchronizuje schemat bazy danych z definicjami encji przy kazdym uruchomieniu aplikacji. Moze to usunac kolumny, zmienic typy danych lub skasowac tabele bez jakiejkolwiek dokumentacji tych zmian. W srodowiskach produkcyjnych prowadzi to do utraty danych. Migracje oferuja natomiast wersjonowane, odtwarzalne i odwracalne zmiany schematu.

Jaka jest roznica miedzy migration:generate a migration:create?

Komenda migration:generate porownuje aktualne definicje encji ze schematem bazy danych i automatycznie generuje niezbedne instrukcje SQL. migration:create tworzy natomiast pusty plik migracji, w ktorym instrukcje SQL sa pisane recznie. Ta druga opcja jest przydatna przy migracjach danych lub operacjach seedowania.

Po ktorej stronie znajduje sie klucz obcy w relacji OneToMany/ManyToOne?

Klucz obcy znajduje sie zawsze po stronie @ManyToOne. Strona @OneToMany nie tworzy zadnej kolumny w bazie danych — sluzy jedynie do odwzorowania relacji zwrotnej w TypeScript.

Kiedy stosowac QueryBuilder zamiast Repository API?

Repository API (find, findOne, save) nadaje sie do prostych operacji CRUD i zapytan z bezposrednimi warunkami filtrowania. QueryBuilder jest stosowany, gdy potrzebne sa zlozone joiny, podzapytania, funkcje agregujace lub dynamicznie konstruowane zapytania.

Jak rozwiazywac zaleznosci kolowe miedzy modulami?

NestJS udostepnia forwardRef() do obslugi zaleznosci kolowych miedzy modulami. Jednak ich wystepowanie czesto wskazuje na problem architektoniczny. Lepszym rozwiazaniem jest wydzielenie wspoldzielonej logiki do osobnego modulu.

Co sie dzieje, gdy wewnatrz transakcji uzywa sie standardowego repozytorium zamiast managera?

Operacje wykonywane przez standardowe repozytorium przebiegaja poza transakcja. Oznacza to, ze zmiany sa natychmiast zatwierdzane (commitowane) i nie moga byc wycofane w przypadku bledu. Prowadzi to do niespojnosci danych.

Jaka jest roznica miedzy @JoinColumn a @JoinTable?

@JoinColumn jest uzywany w relacjach OneToOne i ManyToOne do wskazania strony bedacej wlascicielem klucza obcego. @JoinTable jest stosowany wylacznie w relacjach ManyToMany i definiuje tabele posrednia (junction table) laczaca obie strony relacji.

Jakie sa strategie ladowania relacji w TypeORM?

TypeORM oferuje dwie glowne strategie: eager loading (relacja jest automatycznie ladowana przy kazdym zapytaniu) i lazy loading (relacja jest ladowana dopiero przy pierwszym dostepie, wymaga uzycia Promise). W praktyce najczesciej stosuje sie jawne wskazanie relacji do zaladowania za pomoca tablicy relations lub QueryBuildera.

Jaka jest roznica miedzy forRoot a forFeature w TypeOrmModule?

forRoot() konfiguruje polaczenie z baza danych i jest wywolywany raz w module glownym. forFeature() rejestruje encje w kontekscie danego modulu funkcjonalnego, udostepniajac odpowiednie repozytoria do wstrzykiwania.

Czym rozni sie save od insert w TypeORM?

save sprawdza, czy encja juz istnieje (na podstawie klucza glownego) — jesli tak, wykonuje aktualizacje; jesli nie, tworzy nowy rekord. insert zawsze tworzy nowy rekord i jest szybsze, poniewaz nie wymaga uprzedniego zapytania. Jednak insert nie wywoluje subskrypcji encji ani kaskadowych zapisow.

Gotowy na rozmowy o Node.js / NestJS?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Podsumowanie

Polaczenie NestJS z TypeORM tworzy dojrzaly ekosystem do typobezpiecznego programowania backendowego w Node.js. Kluczowe wnioski mozna podsumowac nastepujaco:

  • Migracje zastepuja synchronize w kazdym srodowisku wykraczajacym poza lokalna prace deweloperska
  • Relacje wymagaja jasnego zrozumienia, ktora strona jest wlascicielem klucza obcego
  • Transakcje musza konsekwentnie przebiegac przez managera, a nie przez standardowe repozytoria
  • QueryBuilder uzupelnia Repository API tam, gdzie proste metody osiagaja swoje ograniczenia
  • Na rozmowach technicznych liczy sie nie tylko znajomosc skladni, ale przede wszystkim umiejetnosc uzasadnienia decyzji architektonicznych

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Udostępnij

Powiązane artykuły