Що таке хуки життєвого циклу в NestJS?
Хуки (lifecycle hooks) життєвого циклу в NestJS - це інтерфейси, що дозволяють провайдерам запускати код у конкретні моменти ініціалізації модулів і завершення роботи застосунку.
Теорія
TL;DR
OnModuleInitспрацьовує після того, як провайдери модуля готові, до початку прослуховування запитівOnApplicationBootstrapспрацьовує після ініціалізації ВСІХ модулів, передapp.listen()- Хуки завершення потребують
app.enableShutdownHooks()- без цього SIGTERM просто завершує процес - Порядок виконання: дочірні модулі першими, потім батьківські, потім рівень застосунку; завершення у зворотньому порядку
- Правило вибору:
OnModuleInitдля підключення до БД,OnApplicationBootstrapдля задач, що потребують готовності всіх модулів
Швидкий приклад
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
// НЕПРАВИЛЬНО
@Injectable({ scope: Scope.REQUEST })
export class UserService implements OnModuleInit {
onModuleInit() { /* підключення до БД */ }
}
// хук спрацьовує на кожен запит - витік з'єднань
// ПРАВИЛЬНО: перенести до singleton-провайдераRequest-scoped провайдер створюється і знищується при кожному HTTP-запиті, тому хук спрацьовуватиме тисячі разів. Перенеси ініціалізацію до сервісу без REQUEST scope.
Помилка 2: блокування bootstrap синхронними важкими операціями
// НЕПРАВИЛЬНО
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
// НЕПРАВИЛЬНО - OtherModuleService може бути ще не готовий
@Injectable()
export class AppService implements OnModuleInit {
constructor(@Inject(OtherModuleService) private svc: OtherModuleService) {}
onModuleInit() {
this.svc.doSetup(); // race condition
}
}
// ПРАВИЛЬНО - використовуй OnApplicationBootstrapПомилка 4: відсутність app.enableShutdownHooks()
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks(); // без цього хуки завершення не спрацюють
await app.listen(3000);
}На практиці - це найпоширеніша причина втрати даних при перезапуску NestJS-сервісу в Kubernetes. Застосунок запускається нормально, але при rolling deploy з'єднання просто обривається без жодного cleanup.
Помилка 5: відсутність await в асинхронних хуках завершення
// НЕПРАВИЛЬНО
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 з повним очищенням
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 drainedOnModuleInit і OnModuleDestroy як пара дають чіткий контракт підключення і відключення. quit() зливає незавершені команди перед закриттям сокету, запобігаючи втраті даних.
Коректне завершення черги з обробкою сигналу
// 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
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.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.