Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке хуки життєвого циклу в NestJS?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Хуки (lifecycle hooks) життєвого циклу в NestJS** - це інтерфейси, що дозволяють провайдерам виконувати код у конкретні моменти запуску й завершення роботи застосунку. `OnModuleInit` спрацьовує після готовності провайдерів модуля; `OnApplicationBootstrap` - після ініціалізації всіх модулів, перед `app.listen()`. ```typescript @Injectable() export class DatabaseService implements OnModuleInit { async onModuleInit() { await this.connect(); // викликається один раз, після DI } } ``` **Ключове:** без `app.enableShutdownHooks()` в `main.ts` хуки завершення не спрацюють.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Хуки (lifecycle hooks) життєвого циклу в NestJS** - це інтерфейси, що дозволяють провайдерам запускати код у конкретні моменти ініціалізації модулів і завершення роботи застосунку. ## Теорія ### TL;DR - `OnModuleInit` спрацьовує після того, як провайдери модуля готові, до початку прослуховування запитів - `OnApplicationBootstrap` спрацьовує після ініціалізації ВСІХ модулів, перед `app.listen()` - Хуки завершення потребують `app.enableShutdownHooks()` - без цього SIGTERM просто завершує процес - Порядок виконання: дочірні модулі першими, потім батьківські, потім рівень застосунку; завершення у зворотньому порядку - Правило вибору: `OnModuleInit` для підключення до БД, `OnApplicationBootstrap` для задач, що потребують готовності всіх модулів ### Швидкий приклад ```typescript import { Injectable, OnModuleInit, Logger } from '@nestjs/common'; @Injectable() export class DatabaseService implements OnModuleInit { private readonly logger = new Logger(DatabaseService.name); async onModuleInit() { await this.connect(); this.logger.log('DB connected after DI resolved'); // Виконується один раз, після провайдерів, до app.listen() } private async connect() { /* встановити пул з'єднань */ } } ``` NestJS викликає `onModuleInit` один раз, після того як DI-контейнер розв'язав усі залежності модуля. Жодного HTTP-запиту ще не надійшло. ### Рівні хуків Хуки діляться на дві категорії. Хуки рівня модуля (`OnModuleInit`, `OnModuleDestroy`) спрацьовують окремо для кожного модуля під час його завантаження. Хуки рівня застосунку (`OnApplicationBootstrap`, `BeforeApplicationShutdown`, `OnApplicationShutdown`) спрацьовують один раз для всього застосунку, після готовності кожного модуля. Це важливо при міжмодульних залежностях. Якщо `ServiceA` залежить від `ServiceB` з іншого модуля, і обидва реалізують `OnModuleInit`, порядок виконання не гарантований. Потрібен `OnApplicationBootstrap` - на цей момент усі модулі вже завершили ініціалізацію. ### Всі п'ять хуків | Хук | Метод | Коли викликається | |-----|-------|-------------------| | `OnModuleInit` | `onModuleInit()` | Після створення провайдерів модуля | | `OnApplicationBootstrap` | `onApplicationBootstrap()` | Після всіх модулів, до початку прослуховування | | `OnModuleDestroy` | `onModuleDestroy()` | Після виклику `app.close()` | | `BeforeApplicationShutdown` | `beforeApplicationShutdown(signal?)` | Перед закриттям з'єднань, отримує сигнал ОС | | `OnApplicationShutdown` | `onApplicationShutdown(signal?)` | Після закриття всіх з'єднань | ### Коли використовувати - **Підключення до БД або Redis**: `OnModuleInit` - провайдери готові, міжмодульні залежності не потрібні - **Фонові воркери або cron-задачі**: `OnApplicationBootstrap` - усі модулі ініціалізовані, порядок гарантований - **Коректне завершення (злив пулів, флаш логів)**: `OnApplicationShutdown` + `app.enableShutdownHooks()` - **Швидка підготовка до завершення (зупинити прийом задач)**: `BeforeApplicationShutdown`, тримати синхронним або швидко асинхронним - **Логіка на запит**: хуки не підходять, використовуй guards або interceptors ### Як це працює всередині NestJS сканує провайдери на наявність реалізованих інтерфейсів хуків під час інстанціювання модуля. Знайдені хуки додаються до впорядкованого списку залежно від рівня (модуль чи застосунок) і викликаються через `await` під час `NestApplication.init()` або `close()`. Для завершення `process.on('SIGTERM')` і `process.on('SIGINT')` запускають ланцюжок, але лише після `app.enableShutdownHooks()`. DI-контейнер відстежує провайдери з хуками через Reflect metadata, тому `implements` - не просто TypeScript-формальність. Якщо метод відсутній на прототипі, NestJS його не викличе. Порядок при запуску: `OnModuleInit` дочірніх модулів, потім батьківських, потім `OnApplicationBootstrap` для всього застосунку. При завершенні: `BeforeApplicationShutdown` (швидка підготовка), потім сервер зупиняє прийом з'єднань, потім `OnModuleDestroy`, потім `OnApplicationShutdown`. ### Поширені помилки **Помилка 1: хуки у провайдерах з REQUEST scope** ```typescript // НЕПРАВИЛЬНО @Injectable({ scope: Scope.REQUEST }) export class UserService implements OnModuleInit { onModuleInit() { /* підключення до БД */ } } // хук спрацьовує на кожен запит - витік з'єднань // ПРАВИЛЬНО: перенести до singleton-провайдера ``` Request-scoped провайдер створюється і знищується при кожному HTTP-запиті, тому хук спрацьовуватиме тисячі разів. Перенеси ініціалізацію до сервісу без REQUEST scope. **Помилка 2: блокування bootstrap синхронними важкими операціями** ```typescript // НЕПРАВИЛЬНО onModuleInit() { const data = fs.readFileSync('./config.json'); // блокує event loop } // ПРАВИЛЬНО async onModuleInit() { const data = await fs.promises.readFile('./config.json'); } ``` Синхронна важка задача в `onModuleInit` заморожує весь bootstrap. На Heroku або подібних платформах це призводить до dyno timeout ще до запуску застосунку. **Помилка 3: міжмодульні залежності в OnModuleInit** ```typescript // НЕПРАВИЛЬНО - OtherModuleService може бути ще не готовий @Injectable() export class AppService implements OnModuleInit { constructor(@Inject(OtherModuleService) private svc: OtherModuleService) {} onModuleInit() { this.svc.doSetup(); // race condition } } // ПРАВИЛЬНО - використовуй OnApplicationBootstrap ``` **Помилка 4: відсутність app.enableShutdownHooks()** ```typescript // main.ts async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableShutdownHooks(); // без цього хуки завершення не спрацюють await app.listen(3000); } ``` На практиці - це найпоширеніша причина втрати даних при перезапуску NestJS-сервісу в Kubernetes. Застосунок запускається нормально, але при rolling deploy з'єднання просто обривається без жодного cleanup. **Помилка 5: відсутність await в асинхронних хуках завершення** ```typescript // НЕПРАВИЛЬНО onApplicationShutdown(signal: string) { closeDB(); // без await, процес завершується раніше } // ПРАВИЛЬНО async onApplicationShutdown(signal: string) { if (signal === 'SIGTERM') { await closeDB(); } } ``` Одна команда таким чином втратила 10k задач у черзі до того, як додала `async/await` у хук завершення. ### Де зустрічається - **TypeORM**: `OnModuleInit` для `dataSource.initialize()` - такий самий патерн в офіційних docs NestJS - **@nestjs/bull**: `OnApplicationBootstrap` для запуску Redis-воркерів після всіх модулів - **Prisma**: `BeforeApplicationShutdown` для `prisma.$disconnect()` - **Winston**: `OnModuleDestroy` для флашу транспортів перед завершенням процесу - **Redis cache**: `OnModuleInit` для підключення, `OnModuleDestroy` для зливу пулу ### Питання на співбесіді **Q:** Який порядок виконання хуків між кількома модулями? **A:** Depth-first при запуску: `OnModuleInit` дочірніх модулів спрацьовує раніше батьківських. `OnApplicationBootstrap` виконується після завершення всіх `OnModuleInit`. При завершенні порядок зворотній. **Q:** Чи працюють lifecycle hooks у динамічних модулях? **A:** Так. Хуки реєструються на кожен інстанс, тому динамічно завантажені модулі реєструють свої хуки під час завантаження. Lazy-модулі затримують реєстрацію до виклику `LazyModuleLoader.load()`. **Q:** Що станеться, якщо хук викине помилку? **A:** Помилка поширюється вгору і перериває bootstrap. NestJS логує stack trace. Якщо потрібна помилкостійкість, загорни ризиковані операції в `try/catch` всередині хука. **Q:** Чому не використовувати `process.on('SIGTERM')` напряму замість хуків завершення? **A:** Можна, але хуки NestJS дають автоматичний порядок і DI-контекст. Raw `process.on` слухачі запускаються поза lifecycle NestJS, тому немає гарантії, що всі модулі завершили роботу до очищення ресурсів. **Q:** Як NestJS виявляє, які провайдери реалізують інтерфейс хука під час виконання? (рівень senior) **A:** Перевіряє прототипи через інспекцію на кшталт `'onModuleInit' in provider` разом з Reflect metadata під час discovery провайдерів. Тому відсутність `implements OnModuleInit` у TypeScript не зупинить хук - якщо метод є на прототипі, NestJS його викличе. ## Приклади ### Підключення Redis з повним очищенням ```typescript import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { createClient } from 'redis'; @Injectable() export class CacheService implements OnModuleInit, OnModuleDestroy { private client = createClient(); async onModuleInit() { await this.client.connect(); console.log('Redis ready'); } async onModuleDestroy() { await this.client.quit(); // зливає незавершені команди перед закриттям сокету console.log('Redis pool drained'); } } // Вивід при SIGTERM: Redis pool drained ``` `OnModuleInit` і `OnModuleDestroy` як пара дають чіткий контракт підключення і відключення. `quit()` зливає незавершені команди перед закриттям сокету, запобігаючи втраті даних. ### Коректне завершення черги з обробкою сигналу ```typescript // main.ts async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableShutdownHooks(); // обов'язково для SIGTERM/SIGINT await app.listen(3000); } // queue.service.ts import { Injectable, BeforeApplicationShutdown, OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class QueueService implements BeforeApplicationShutdown, OnApplicationShutdown { private accepting = true; beforeApplicationShutdown(signal: string) { console.log(`Отримано сигнал: ${signal}`); this.accepting = false; // зупиняємо прийом нових задач } async onApplicationShutdown(signal: string) { if (signal === 'SIGTERM') { await this.drainActiveJobs(); // повільна асинхронна частина } console.log('Черга злита, завершення роботи'); } private drainActiveJobs() { return new Promise<void>(resolve => setTimeout(resolve, 2000)); } } ``` `BeforeApplicationShutdown` обробляє швидку синхронну підготовку (зупинку прийому), а `OnApplicationShutdown` - повільну асинхронну частину (злив черги). Такий розподіл не блокує послідовність завершення. ### Наповнення бази даних у dev-середовищі через OnApplicationBootstrap ```typescript import { Injectable, OnApplicationBootstrap } from '@nestjs/common'; @Injectable() export class SeedService implements OnApplicationBootstrap { constructor( private readonly usersService: UsersService, private readonly configService: ConfigService, ) {} async onApplicationBootstrap() { if (this.configService.get('NODE_ENV') !== 'development') return; const count = await this.usersService.count(); if (count === 0) { await this.usersService.create({ name: 'Admin', email: 'admin@app.com' }); console.log('Dev seed complete'); } } } ``` `OnApplicationBootstrap` - правильний вибір тут, бо `SeedService` залежить від `UsersService` з іншого модуля. До моменту спрацювання `onApplicationBootstrap` весь DI-граф розв'язаний і кожен модуль завершив `OnModuleInit`.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.