NestJS i TypeORM w 2026 roku: migracje, relacje i pytania rekrutacyjne
Opanuj integrację NestJS TypeORM z migracjami TypeORM 1.1, relacjami encji, wzorcami repozytoriów i typowymi pytaniami rekrutacyjnymi dla backend developerów.

NestJS TypeORM pozostaje jedną z najlepiej sprawdzonych integracji ORM dla korporacyjnych backendów Node.js. Po wydaniu TypeORM w wersji 1.0 w maju 2026 roku, a następnie serii poprawek 1.1.x rozwiązujących problemy z obsługą połączeń i persystencją relacji, oraz po zakończeniu przez NestJS 12 migracji na ESM, stos ten osiągnął dojrzałość jako stabilna podstawa dla aplikacji produkcyjnych. Niniejszy przewodnik omawia kluczowe wzorce, które powinien opanować każdy backend developer: migracje, relacje między encjami, wzorzec repozytorium oraz pytania rekrutacyjne z nimi związane.
NestJS 12 dostarcza podstawowe pakiety jako ESM, wymagając Node.js 20.19+ lub 22.12+. Pakiet @nestjs/typeorm v12 podąża za tym samym modelem. TypeORM 1.1.2 naprawia wycieki połączeń w DataSource.initialize() oraz koryguje persystencję wierszy tabeli łączącej w relacjach many-to-many, gdy relacje nie są załadowane.
Konfiguracja migracji TypeORM w NestJS 12
Pierwszą zasadą dla produkcyjnych baz danych jest: nigdy nie używać synchronize: true. Auto-sync porównuje metadane encji z aktualnym schematem i wprowadza zmiany bezpośrednio, co grozi utratą danych przy usuwaniu kolumn, zmianie typów czy zmianach nazw pól. Migracje zapewniają wersjonowaną, możliwą do przeglądania i odwracalną alternatywę.
TypeORM 1.x wymaga dedykowanego pliku konfiguracyjnego DataSource dla CLI, oddzielonego od konfiguracji modułu NestJS. CLI nie może rozwiązywać wstrzykiwania zależności NestJS, dlatego potrzebuje samodzielnego punktu wejścia.
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,
});Konfiguracja modułu NestJS odzwierciedla te same ustawienia, ale wykorzystuje wzorzec TypeOrmModule.forRootAsync z wstrzykiwaniem zależności:
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 zastępuje ręczną tablicę entities, automatycznie rejestrując każdą encję zaimportowaną przez TypeOrmModule.forFeature() w modułach funkcjonalnych.
Przepływ pracy z migracjami: generowanie, przegląd, uruchamianie
TypeORM 1.x dostarcza typeorm-ts-node-commonjs jako zalecany runner CLI, zastępując starsze podejście ts-node ./node_modules/typeorm/cli. Dodaj te skrypty do package.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"
}Komenda generate porównuje metadane encji z aktualnym schematem bazy danych i generuje instrukcje SQL. Wygenerowany plik zawsze należy przejrzeć przed zatwierdzeniem. TypeORM wykrywa dodawanie i usuwanie kolumn, zmiany typów, modyfikacje indeksów i zmiany kluczy obcych, ale nie obsługuje backfillów danych, zmian nazw tabel (które są interpretowane jako usunięcie i utworzenie nowej) oraz operacji związanych z rozszerzeniami bazy danych lub triggerami.
Zmiana nazwy kolumny generuje DROP + ADD zamiast ALTER RENAME. To powoduje utratę istniejących danych. SQL w metodzie up() zawsze należy sprawdzać i w razie potrzeby korygować ręcznie.
Relacje między encjami: OneToMany, ManyToOne, ManyToMany
Relacje encji TypeORM mapują się bezpośrednio na klucze obce SQL za pomocą dekoratorów. Zrozumienie trzech podstawowych typów relacji i ich opcji jest fundamentalne dla każdego modułu bazodanowego NestJS.
User z wieloma rekordami Order demonstruje najczęstszy wzorzec:
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;
}Strona @ManyToOne jest właścicielem klucza obcego. @JoinColumn jest opcjonalny dla @ManyToOne (TypeORM go wywnioskuje), ale jawne nazewnictwo zapobiega niespodziankom przy raw query lub diffach migracji.
Dla relacji many-to-many TypeORM automatycznie tworzy tabelę łączącą:
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 musi pojawić się dokładnie po jednej stronie relacji. Określa, która encja jest właścicielem tabeli łączącej i kontroluje jej nazwę.
Gotowy na rozmowy o Node.js / NestJS?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
Wzorzec Repository i własne repozytoria
System modułów i DI w NestJS integruje repozytoria TypeORM przez TypeOrmModule.forFeature(). Wstrzykiwanie domyślnego Repository<T> pokrywa standardowe operacje CRUD. Gdy zapytania stają się złożone, własne repozytoria enkapsulują tę logikę.
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 {}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();
}
}API find() obsługuje proste zapytania. QueryBuilder obsługuje joiny, podzapytania, agregacje i wszystko, co wymaga szczegółowej kontroli nad SQL. Łączenie obu w tym samym serwisie jest całkowicie poprawne: używaj find() dla prostych wyszukiwań i QueryBuilder, gdy zapytania nie da się wyrazić przez API opcji.
Zarządzanie transakcjami dla integralności danych
Operacje modyfikujące wiele tabel wymagają transakcji. TypeORM oferuje dwa podejścia: metodę DataSource.transaction() i API QueryRunner.
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);
});
}Metoda transaction() oparta na callbacku automatycznie commituje przy sukcesie i wycofuje przy każdym rzuconym błędzie. Każda operacja bazodanowa wewnątrz callbacka musi używać dostarczonego manager zamiast wstrzykniętego repozytorium, w przeciwnym razie te operacje wykonają się poza transakcją.
DataSource.transaction() sprawdza się w prostych wielokrokowych operacjach. QueryRunner oferuje ręczną kontrolę nad punktami commit/rollback oraz savepointami, przydatną przy częściowych wycofaniach w przetwarzaniu wsadowym.
Najczęstsze pytania rekrutacyjne dotyczące NestJS TypeORM
Te pytania regularnie pojawiają się na rozmowach dla backend developerów. Każda odpowiedź jest ukierunkowana na głębokość oczekiwaną w technicznej dyskusji na poziomie seniora.
Dlaczego synchronize: true nie powinno być nigdy używane w produkcji?
Auto-sync stosuje destrukcyjne zmiany schematu bez potwierdzenia. Usunięcie kolumny, zmiana typu czy usunięcie indeksu następuje natychmiast na żywych danych. Migracje zapewniają kontrolę wersji, code review i możliwość wycofania. Jedynym bezpiecznym zastosowaniem synchronize jest lokalne środowisko deweloperskie z danymi, które można utracić.
Jaka jest różnica między relacjami eager i lazy?
Relacje eager ładują się automatycznie przy każdym zapytaniu do encji nadrzędnej. Relacje lazy zwracają Promise i wykonują osobne zapytanie dopiero przy dostępie. Eager loading powoduje overhead typu N+1, gdy powiązane dane nie są potrzebne. Lazy loading odkłada koszt, ale może wyzwalać nieoczekiwane zapytania głęboko w stosie wywołań. Jawna opcja relations w find() lub leftJoinAndSelect w QueryBuilder daje pełną kontrolę nad tym, co jest ładowane dla każdego zapytania.
Jak działa cascade w relacjach TypeORM?
Ustawienie cascade: true na relacji oznacza, że zapisanie encji nadrzędnej zapisuje również niezapisane encje potomne. cascade: ['insert', 'update'] ogranicza zachowanie do konkretnych operacji. Kaskady działają na poziomie ORM, nie bazy danych. Dla kaskadowego usuwania na poziomie bazy danych używa się onDelete: 'CASCADE' w dekoratorze @ManyToOne, co generuje constraint ON DELETE CASCADE w migracji.
Kiedy należy użyć własnego repozytorium zamiast domyślnego Repository<T>?
Własne repozytoria enkapsulują złożoną logikę zapytań, która nie należy do serwisów. Sygnały do wydzielenia własnego repozytorium: ten sam łańcuch QueryBuilder pojawia się w wielu serwisach, zapytanie obejmuje wiele joinów lub podzapytań, lub zapytanie wymaga surowego SQL. Proste wywołania find() nie uzasadniają własnego repozytorium.
Więcej pytań o architekturę NestJS znajdziesz w kompletnym przewodniku po guards i interceptors NestJS.
Gotowy na rozmowy o Node.js / NestJS?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
Źródła
- Wydanie TypeORM 1.0.0 - wymóg ECMAScript 2023, usunięcie Connection, ściślejsze domyślne walidacje
- Wydanie TypeORM 1.1.2 - naprawa wycieku połączeń w DataSource.initialize(), poprawki wierszy tabeli łączącej many-to-many
- Wydanie NestJS 12 - migracja na ESM, wymóg Node.js 20.19+/22.12+
- @nestjs/typeorm 12.0.0 - natywne moduły ES, kompatybilność z CommonJS przez Node require(esm)
Kluczowe wnioski dla aplikacji produkcyjnych NestJS TypeORM
- Używaj oddzielnego pliku konfiguracyjnego
DataSourcedla CLI TypeORM, niezależnego od konfiguracji modułu NestJS - Nigdy nie włączaj
synchronizew środowisku produkcyjnym; polegaj na generowanych i przeglądanych migracjach dla wszystkich zmian schematu - Umieszczaj
@ManyToOnena encji będącej właścicielem klucza obcego i używaj jawnego nazewnictwa@JoinColumndla przejrzystości w raw query i wynikach migracji - Wybieraj między API opcji
find()dla prostych zapytań aQueryBuilderdla złożonych joinów, agregacji i logiki warunkowej - Opakowuj wielotabelowe mutacje w transakcje używając
DataSource.transaction()i upewnij się, że każda operacja wewnątrz callbacka używa transakcyjnegomanager - Wraz ze stabilizacją API przez TypeORM 1.1.x oraz zakończeniem migracji na ESM przez NestJS 12, stos ten pozostaje solidnym wyborem dla korporacyjnych backendów wymagających ustrukturyzowanego, typobezpiecznego dostępu do bazy danych
Zacznij ćwiczyć!
Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.
Znajdziesz błąd w Node.js / NestJS?
Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Autor:
Anthony Fillion-MailletZałożyciel SharpSkill
Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.
Zaktualizowano 21 października 2026
Tagi
Udostępnij
Powiązane artykuły

NestJS + Prisma: nowoczesny stack backendowy dla Node.js
Kompletny przewodnik po budowaniu nowoczesnego API backendowego z NestJS i Prismą. Konfiguracja, modele, serwisy, transakcje i dobre praktyki.

NestJS: Budowanie kompletnego REST API
Kompletny przewodnik po budowaniu profesjonalnego REST API z NestJS. Kontrolery, serwisy, moduły, walidacja z class-validator i obsługa błędów z praktycznymi przykładami.

NestJS i GraphQL w 2026: Schematy, Resolvery i Pytania Rekrutacyjne
Kompletny poradnik NestJS GraphQL omawiający podejścia schema-first i code-first, resolvery, DataLoader oraz pytania rekrutacyjne w 2026 roku.