# NestJS та TypeORM у 2026 році: міграції, зв'язки та питання на співбесідах > NestJS у поєднанні з TypeORM залишається одним із найбільш зрілих стеків для Node.js-бекенду. Стаття розглядає міграції, зв'язки між сутностями, транзакції та типові питання технічних співбесід. - Published: 2026-06-19 - Updated: 2026-06-19 - Author: SharpSkill - Reading time: 5 min --- NestJS та TypeORM у 2026 році утворюють один із найбільш зрілих тандемів у світі бекенд-розробки на Node.js. NestJS вирізняється модульною архітектурою та вбудованою системою ін'єкції залежностей, тоді як TypeORM надає інструменти для типобезпечних операцій з базою даних. Водночас у щоденній проєктній практиці — особливо у сфері міграцій, складних зв'язків та взаємодії обох технологій — виникають питання, які регулярно з'являються і на технічних співбесідах. Ця стаття розглядає ключові концепції, пов'язані з міграціями TypeORM у проєктах NestJS, описує різні типи зв'язків на основі конкретних прикладів коду та завершується набором практичних питань для співбесід, що поглиблюють розуміння теми. > **TypeORM DataSource API** > > TypeORM кардинально переробив своє API конфігурації. Проєкти, які досі використовують `ormconfig.json` або `createConnection`, мають перейти на `DataSource` API. Усі приклади в цій статті використовують актуальну версію API. ## Налаштування TypeORM у NestJS Першим кроком є створення автономної конфігурації DataSource. Цей файл використовується CLI TypeORM для роботи з міграціями. Розділення конфігурації дозволяє CLI працювати незалежно від контексту 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, }); ``` Властивість `synchronize: false` заслуговує на особливу увагу. У режимі розробки може виникнути спокуса встановити `synchronize: true`, щоб TypeORM автоматично оновлював схему бази даних. Однак у продакшн-середовищах це призводить до втрати даних та непередбачуваної поведінки. Натомість використовуються міграції. У `AppModule` TypeORM інтегрується через асинхронну конфігурацію, де `ConfigService` надає змінні оточення: ```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 {} ``` Різниця між `config.get()` і `config.getOrThrow()` тонка, але суттєва: `getOrThrow()` кидає виняток, якщо змінна відсутня. Для критичних конфігураційних значень, таких як порт бази даних, це гарантує миттєве виявлення помилок конфігурації замість тихого використання значень за замовчуванням. ## Міграції: створення, виконання та відкат Міграції становлять основу будь-якої професійної стратегії управління базою даних. Вони документують зміни схеми у версіонованому та відтворюваному вигляді. Наступні npm-скрипти спрощують роботу з 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" } ``` Практичний робочий процес виглядає так: спочатку модифікуються сутності, потім `npm run migration:generate --name=AddUserTable` автоматично генерує файл міграції, що відображає різницю між поточною схемою та визначеннями сутностей. Команда `npm run migration:run` застосовує міграцію, а `npm run migration:revert` скасовує останню застосовану міграцію. Поширеною помилкою у проєктах є ручне написання міграцій, хоча `migration:generate` виконує порівняння автоматично. Ручні міграції мають сенс лише тоді, коли необхідна трансформація даних — наприклад, при перейменуванні стовпця з одночасною міграцією даних. > **Перевірка згенерованих міграцій** > > Перейменування стовпця генерує операцію DROP + ADD замість ALTER RENAME. Це знищує наявні дані. SQL-інструкції в методі `up()` завжди слід перевіряти і за потреби коригувати вручну. ## Моделювання сутностей та зв'язків TypeORM використовує декоратори для відображення структур бази даних безпосередньо у класах TypeScript. Три найважливіші типи зв'язків — це OneToMany/ManyToOne, ManyToMany та OneToOne. ### OneToMany та ManyToOne Класичний зв'язок між користувачами та замовленнями демонструє взаємодію `@OneToMany` і `@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; } ``` Ключовий момент: сторона `@OneToMany` не створює жодного стовпця у базі даних. Зовнішній ключ завжди знаходиться на стороні `@ManyToOne`. Декоратор `@JoinColumn` явно визначає назву стовпця зовнішнього ключа, що підвищує читабельність бази даних та запобігає конфліктам імен. Опція `onDelete: 'CASCADE'` забезпечує автоматичне видалення замовлень при видаленні пов'язаного користувача. ### ManyToMany Для продуктів і категорій підходить зв'язок ManyToMany. TypeORM автоматично створює проміжну таблицю: ```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[]; } ``` Декоратор `@JoinTable` позначає сторону, що володіє зв'язком. Лише одна сторона може мати цей декоратор. Якщо проміжна таблиця має містити додаткові стовпці (наприклад, порядок сортування), замість цього потрібно створити окрему сутність із двома зв'язками `@ManyToOne`. ## Патерн Repository та функціональні модулі NestJS заохочує модуляризацію через функціональні модулі. Кожен модуль реєструє свої сутності за допомогою `TypeOrmModule.forFeature()` і отримує доступ до відповідних репозиторіїв: ```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 {} ``` У сервісі репозиторій ін'єктується за допомогою `@InjectRepository`. Для простих запитів достатньо Repository API, тоді як QueryBuilder застосовується у складніших сценаріях: ```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(); } } ``` Метод `findByUser` демонструє, як фільтрувати зв'язки за допомогою вкладених умов Where. Масив `relations` керує завантаженням пов'язаних сутностей. Без цього оголошення TypeORM поверне лише замовлення без даних користувача. QueryBuilder у `findWithFilters` надає більше контролю: `leftJoinAndSelect` завантажує зв'язок і робить його доступним у результатах, а параметризовані запити надійно запобігають атакам SQL injection. ## Правильне використання транзакцій Коли кілька операцій з базою даних мають виконуватися атомарно, транзакції є незамінними. TypeORM пропонує для цього метод `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); }); } ``` Усередині транзакції для всіх операцій використовується виключно `manager`. Якби натомість було використано звичайний репозиторій, ці операції виконувалися б поза транзакцією, і помилка на наступному кроці могла б призвести до неузгоджених даних. У разі помилки всередині колбеку вся транзакція автоматично відкочується. > **QueryRunner vs transaction()** > > `DataSource.transaction()` підходить для простих багатокрокових операцій. `QueryRunner` надає ручне керування точками commit/rollback та savepoint'ами — корисно при часткових відкатах у пакетній обробці. ## Найпоширеніші питання на співбесідах щодо NestJS та TypeORM Технічні співбесіди з NestJS та TypeORM зазвичай охоплюють три сфери: розуміння архітектури, практичне розв'язання проблем та уникнення типових помилок. **Чому `synchronize` має бути вимкнено у продакшні?** Опція `synchronize: true` автоматично синхронізує схему бази даних із визначеннями сутностей при кожному запуску застосунку. Це може видалити стовпці, змінити типи даних або знищити таблиці без жодної документації цих змін. У продакшн-середовищах це призводить до втрати даних. Міграції натомість забезпечують версіоновані, відтворювані та оборотні зміни схеми. **Яка різниця між `migration:generate` та `migration:create`?** Команда `migration:generate` порівнює поточні визначення сутностей зі схемою бази даних і автоматично генерує необхідні SQL-інструкції. `migration:create` створює порожній файл міграції, в якому SQL-команди пишуться вручну. Останній варіант корисний для міграцій даних або операцій заповнення (seeding). **На якій стороні знаходиться зовнішній ключ у зв'язку OneToMany/ManyToOne?** Зовнішній ключ завжди знаходиться на стороні `@ManyToOne`. Сторона `@OneToMany` не створює жодного стовпця у базі даних — вона слугує лише для відображення зворотного зв'язку в TypeScript. **Коли використовувати QueryBuilder замість Repository API?** Repository API (`find`, `findOne`, `save`) підходить для простих CRUD-операцій і запитів із прямими умовами фільтрації. QueryBuilder застосовується, коли потрібні складні join'и, підзапити, агрегатні функції або динамічно побудовані запити. **Як вирішувати циклічні залежності між модулями?** NestJS надає `forwardRef()` для обробки циклічних залежностей між модулями. Проте їх наявність зазвичай свідчить про архітектурну проблему. Кращим рішенням є винесення спільної логіки в окремий модуль. **Що відбувається, якщо всередині транзакції використовується звичайний репозиторій замість manager?** Операції через звичайний репозиторій виконуються поза транзакцією. Це означає, що зміни негайно фіксуються (commit) і не можуть бути відкочені у разі помилки. Це призводить до неузгодженості даних. **Яка різниця між `@JoinColumn` та `@JoinTable`?** `@JoinColumn` використовується у зв'язках OneToOne та ManyToOne для позначення сторони, що володіє зовнішнім ключем. `@JoinTable` застосовується виключно у зв'язках ManyToMany і визначає проміжну таблицю (junction table), що з'єднує обидві сторони зв'язку. **Які стратегії завантаження зв'язків існують у TypeORM?** TypeORM пропонує дві основні стратегії: eager loading (зв'язок автоматично завантажується при кожному запиті) та lazy loading (зв'язок завантажується лише при першому зверненні, потребує використання `Promise`). На практиці найчастіше застосовується явне зазначення зв'язків для завантаження через масив `relations` або QueryBuilder. **Яка різниця між `forRoot` та `forFeature` у TypeOrmModule?** `forRoot()` налаштовує з'єднання з базою даних і викликається один раз у кореневому модулі. `forFeature()` реєструє сутності в контексті конкретного функціонального модуля, надаючи доступ до відповідних репозиторіїв для ін'єкції. **Чим відрізняється `save` від `insert` у TypeORM?** `save` перевіряє, чи існує сутність (за первинним ключем) — якщо так, виконує оновлення; якщо ні, створює новий запис. `insert` завжди створює новий запис і працює швидше, оскільки не потребує попереднього запиту. Проте `insert` не запускає підписки сутностей та каскадні збереження. ## Висновок Поєднання NestJS та TypeORM створює зрілу екосистему для типобезпечної бекенд-розробки на Node.js. Ключові висновки можна підсумувати наступним чином: - **Міграції** замінюють `synchronize` у будь-якому середовищі за межами локальної розробки - **Зв'язки** вимагають чіткого розуміння того, яка сторона володіє зовнішнім ключем - **Транзакції** мають послідовно виконуватися через manager, а не через звичайні репозиторії - **QueryBuilder** доповнює Repository API там, де прості методи досягають своїх обмежень - На технічних співбесідах оцінюється не лише знання синтаксису, а насамперед здатність обґрунтувати архітектурні рішення --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/uk/blog/node-nestjs/nestjs-typeorm-migrations-relations-best-practices