NestJS та TypeORM у 2026 році: міграції, зв'язки та питання на співбесідах
Опануйте інтеграцію NestJS TypeORM з міграціями TypeORM 1.1, зв'язками між сутностями, патернами репозиторіїв та типовими питаннями для backend-розробників.

NestJS TypeORM залишається однією з найбільш перевірених інтеграцій ORM для корпоративних Node.js-бекендів. Після виходу TypeORM версії 1.0 у травні 2026 року, за яким послідувала серія патчів 1.1.x, що виправляють обробку з'єднань та збереження зв'язків, а також після завершення NestJS 12 міграції на ESM, цей стек став зрілою та стабільною основою для продакшн-застосунків. Цей посібник охоплює ключові патерни, які повинен освоїти кожен backend-розробник: міграції, зв'язки між сутностями, патерн репозиторію та питання для співбесід, що з ними пов'язані.
NestJS 12 постачає основні пакети як ESM, вимагаючи Node.js 20.19+ або 22.12+. Пакет @nestjs/typeorm v12 дотримується тієї ж моделі. TypeORM 1.1.2 виправляє витоки з'єднань у DataSource.initialize() та коригує збереження рядків junction-таблиці в many-to-many зв'язках, коли зв'язки не завантажені.
Налаштування міграцій TypeORM у NestJS 12
Перше правило для продакшн-баз даних: ніколи не використовувати synchronize: true. Auto-sync порівнює метадані сутностей з активною схемою та застосовує зміни напряму, що створює ризик втрати даних при видаленні стовпців, зміні типів або перейменуванні полів. Міграції забезпечують версіоновану, доступну для перегляду та оборотну альтернативу.
TypeORM 1.x вимагає окремого конфігураційного файлу DataSource для CLI, відокремленого від конфігурації модуля NestJS. CLI не може використовувати ін'єкцію залежностей 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,
});Конфігурація модуля NestJS відображає ті самі налаштування, але використовує патерн TypeOrmModule.forRootAsync з ін'єкцією залежностей:
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 замінює ручний масив entities, автоматично реєструючи кожну сутність, імпортовану через TypeOrmModule.forFeature() у функціональних модулях.
Робочий процес міграцій: генерація, перегляд, виконання
TypeORM 1.x постачає typeorm-ts-node-commonjs як рекомендований runner CLI, замінюючи старіший підхід ts-node ./node_modules/typeorm/cli. Додайте ці скрипти до 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"
}Команда generate порівнює метадані сутностей з активною базою даних і генерує SQL-інструкції. Завжди переглядайте результат перед комітом. TypeORM виявляє додавання та видалення стовпців, зміни типів, модифікації індексів та зміни зовнішніх ключів, але не виявляє backfill даних, перейменування таблиць (які відображаються як видалення з наступним створенням) та операції з розширеннями бази даних або тригерами.
Перейменування стовпця генерує DROP + ADD замість ALTER RENAME. Це знищує наявні дані. Завжди перевіряйте SQL у методі up() і за потреби коригуйте вручну.
Зв'язки між сутностями: OneToMany, ManyToOne, ManyToMany
Зв'язки сутностей TypeORM безпосередньо відображаються на SQL зовнішні ключі за допомогою декораторів. Розуміння трьох основних типів зв'язків та їх опцій є фундаментальним для будь-якого модуля баз даних NestJS.
User з декількома записами Order демонструє найпоширеніший патерн:
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;
}Сторона @ManyToOne володіє зовнішнім ключем. @JoinColumn є необов'язковим для @ManyToOne (TypeORM виведе його), але явне іменування запобігає несподіванкам при raw-запитах або diff міграцій.
Для many-to-many зв'язків TypeORM автоматично створює junction-таблицю:
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 повинен з'являтися рівно на одній стороні зв'язку. Він визначає, яка сутність володіє junction-таблицею та контролює її ім'я.
Готовий до співбесід з Node.js / NestJS?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Патерн Repository та кастомні репозиторії
Система модулів та DI в NestJS інтегрує репозиторії TypeORM через TypeOrmModule.forFeature(). Ін'єкція стандартного Repository<T> покриває стандартні CRUD-операції. Коли запити стають складними, кастомні репозиторії інкапсулюють цю логіку.
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() обробляє прості запити. QueryBuilder обробляє join'и, підзапити, агрегації та все, що потребує детального контролю над SQL. Використання обох у тому самому сервісі є цілком коректним: find() для простих пошуків і QueryBuilder, коли запит не можна виразити через API опцій.
Управління транзакціями для цілісності даних
Операції, що модифікують декілька таблиць, потребують транзакцій. TypeORM пропонує два підходи: метод DataSource.transaction() та 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);
});
}Метод transaction() на основі колбеку автоматично комітить при успіху та відкочує при будь-якій помилці. Кожна операція з базою даних всередині колбеку повинна використовувати наданий manager замість ін'єктованого репозиторію, інакше ці операції виконуватимуться поза транзакцією.
Використовуйте DataSource.transaction() для простих багатокрокових операцій. Переходьте до QueryRunner, коли потрібен ручний контроль над точками commit/rollback або savepoint'ами, наприклад, для часткових відкатів при пакетній обробці.
Поширені питання на співбесідах щодо NestJS TypeORM
Ці питання регулярно з'являються на співбесідах для backend-розробників. Кожна відповідь спрямована на глибину, очікувану в технічній дискусії рівня senior.
Чому synchronize: true ніколи не слід використовувати в продакшні?
Auto-sync застосовує деструктивні зміни схеми без підтвердження. Видалення стовпця, зміна типу або видалення індексу відбувається негайно на живих даних. Міграції забезпечують контроль версій, code review та можливість відкату. Єдине безпечне використання synchronize — локальна розробка з даними, які можна втратити.
Яка різниця між eager та lazy зв'язками?
Eager-зв'язки завантажуються автоматично з кожним запитом до батьківської сутності. Lazy-зв'язки повертають Promise і виконують окремий запит лише при зверненні. Eager loading спричиняє overhead типу N+1, коли пов'язані дані не потрібні. Lazy loading відкладає витрати, але може викликати неочікувані запити глибоко в стеку викликів. Явна опція relations у find() або leftJoinAndSelect у QueryBuilder дає повний контроль над тим, що завантажується для кожного запиту.
Як працює cascade у зв'язках TypeORM?
Встановлення cascade: true на зв'язку означає, що збереження батьківської сутності також зберігає незбережені дочірні сутності. cascade: ['insert', 'update'] обмежує поведінку конкретними операціями. Каскади працюють на рівні ORM, а не бази даних. Для каскадного видалення на рівні бази даних використовуйте onDelete: 'CASCADE' у декораторі @ManyToOne, що генерує constraint ON DELETE CASCADE у міграції.
Коли слід використовувати кастомний репозиторій замість стандартного Repository<T>?
Кастомні репозиторії інкапсулюють складну логіку запитів, яка не належить сервісам. Ознаки для виділення кастомного репозиторію: той самий ланцюжок QueryBuilder з'являється в декількох сервісах, запит включає декілька join'ів або підзапитів, або запит вимагає raw SQL. Прості виклики find() не виправдовують кастомний репозиторій.
Більше питань про архітектуру NestJS дивіться у повному посібнику з guards та interceptors NestJS.
Готовий до співбесід з Node.js / NestJS?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Джерела
- Реліз TypeORM 1.0.0 - вимога ECMAScript 2023, видалення Connection, суворіші стандартні валідації
- Реліз TypeORM 1.1.2 - виправлення витоку з'єднань у DataSource.initialize(), виправлення рядків junction-таблиці many-to-many
- Реліз NestJS 12 - міграція на ESM, вимога Node.js 20.19+/22.12+
- @nestjs/typeorm 12.0.0 - нативні ES-модулі, сумісність з CommonJS через Node require(esm)
Ключові висновки для продакшн-застосунків NestJS TypeORM
- Використовуйте окремий конфігураційний файл
DataSourceдля CLI TypeORM, незалежний від конфігурації модуля NestJS - Ніколи не вмикайте
synchronizeу продакшн-середовищах; покладайтеся на згенеровані та переглянуті міграції для всіх змін схеми - Розміщуйте
@ManyToOneна сутності, що володіє зовнішнім ключем, і використовуйте явне іменування@JoinColumnдля ясності в raw-запитах та результатах міграцій - Вибирайте між API опцій
find()для простих запитів таQueryBuilderдля складних join'ів, агрегацій та умовної логіки - Обгортайте багатотабличні мутації в транзакції за допомогою
DataSource.transaction()та переконайтеся, що кожна операція всередині колбеку використовує транзакційнийmanager - Зі стабілізацією API у TypeORM 1.1.x та завершенням міграції на ESM у NestJS 12, цей стек залишається потужним вибором для корпоративних бекендів, що потребують структурованого, типобезпечного доступу до бази даних
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Чи знайдеш ти помилку в Node.js / NestJS?
Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Автор:
Anthony Fillion-MailletЗасновник SharpSkill
Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.
Оновлено 21 жовтня 2026 р.
Теги
Поділитися
Пов'язані статті

NestJS + Prisma: сучасний бекенд-стек для Node.js
Повний посібник зі створення сучасного бекенд-API з NestJS і Prisma. Налаштування, моделі, сервіси, транзакції та найкращі практики.

NestJS: Створення повноцінного REST API
Повний посібник зі створення професійного REST API з NestJS. Контролери, сервіси, модулі, валідація з class-validator та обробка помилок з практичними прикладами.

NestJS та GraphQL у 2026: Схеми, Резолвери та Питання на Співбесіді
Повний посібник з інтеграції NestJS GraphQL: підходи schema-first та code-first, резолвери, DataLoader та питання на технічних співбесідах 2026 року.