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 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 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.
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:
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:
{
"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.
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:
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;
}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:
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:
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:
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():
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.
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
synchronizew 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

Node.js 24 w 2026: URLPattern, model uprawnień i pytania rekrutacyjne
Node.js 24 LTS wprowadza stabilny model uprawnień, globalny URLPattern, jawne zarządzanie zasobami z using/await using oraz V8 13.6. Szczegółowy przegląd funkcji istotnych dla produkcji i rozmów kwalifikacyjnych.

Mikroserwisy w NestJS w 2026: architektura, gRPC i pytania rekrutacyjne
Praktyczny przewodnik po architekturze mikroserwisów NestJS z gRPC: granice serwisów, warstwy transportowe, wzorce strumieniowania oraz najczęstsze pytania rekrutacyjne na 2026 rok.

Wydajność Node.js: Event Loop, Klasteryzacja i Optymalizacja w 2026
Optymalizacja wydajności Node.js poprzez zarządzanie pętlą zdarzeń, strategie klasteryzacji i wątki robocze. Praktyczne wzorce dla wysoko obciążonych aplikacji Node.js w 2026 roku.