# 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. - Published: 2026-06-19 - Updated: 2026-06-19 - Author: SharpSkill - Reading time: 5 min --- 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. ```typescript // src/config/typeorm.config.ts 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: ```typescript // src/app.module.ts 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('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: ```json // package.json (scripts section) { "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`: ```typescript // src/entities/user.entity.ts 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; } ``` ```typescript // src/entities/order.entity.ts 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: ```typescript // src/entities/product.entity.ts 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`. ## 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: ```typescript // src/orders/orders.module.ts 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: ```typescript // src/orders/orders.service.ts 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, ) {} async findByUser(userId: string): Promise { 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 { // 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()`: ```typescript // src/orders/orders.service.ts (additional method) async createOrderWithItems( userId: string, items: { productId: string; quantity: number }[], dataSource: DataSource, ): Promise { 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. ## 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 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pl/blog/node-nestjs/nestjs-typeorm-migrations-relations-best-practices