NestJSMiddleTechnical

Как работают lifecycle hooks (onModuleInit, onApplicationShutdown) и где они нужны?

Lifecycle hooks NestJS: onModuleInit выполняется после DI-инициализации модуля (подключение к БД, прогрев кэша), onApplicationShutdown — при завершении процесса (закрытие соединений, flush буферов).

Lifecycle Hooks в NestJS

Полный жизненный цикл приложения

  • onModuleInit — после инициализации зависимостей модуля.
  • onApplicationBootstrap — после инициализации всех модулей.
  • onModuleDestroy — перед уничтожением модуля.
  • beforeApplicationShutdown — получает сигнал OS (SIGTERM и т.д.).
  • onApplicationShutdown — финальная очистка.

onModuleInit — инициализация при старте

import { Injectable, OnModuleInit } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';

@Injectable()
export class DatabaseService implements OnModuleInit {
  constructor(@InjectDataSource() private dataSource: DataSource) {}

  async onModuleInit() {
    // Проверить соединение с БД при старте
    await this.dataSource.query('SELECT 1');
    console.log('Database connection verified');
  }
}

Прогрев кэша

@Injectable()
export class ConfigCacheService implements OnModuleInit {
  private settings: Map<string, string> = new Map();

  constructor(private readonly settingsRepo: SettingsRepository) {}

  async onModuleInit() {
    const all = await this.settingsRepo.findAll();
    for (const s of all) {
      this.settings.set(s.key, s.value);
    }
    console.log(`Loaded ${this.settings.size} settings into cache`);
  }

  get(key: string): string | undefined {
    return this.settings.get(key);
  }
}

onApplicationShutdown — graceful shutdown

// main.ts — обязательно включить!
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks();

// Перехват сигналов OS
process.on('SIGTERM', () => app.close());
process.on('SIGINT', () => app.close());

// queue.service.ts
@Injectable()
export class QueueService implements OnApplicationShutdown {
  private consumer: Consumer;

  async onApplicationShutdown(signal?: string) {
    console.log(`Received shutdown signal: ${signal}`);
    // Дождаться обработки текущих сообщений
    await this.consumer.disconnect();
    await this.producer.disconnect();
  }
}

// http-server.service.ts
@Injectable()
export class ServerService implements BeforeApplicationShutdown {
  beforeApplicationShutdown(signal?: string) {
    console.log(`Preparing for shutdown, signal: ${signal}`);
    // Перестать принимать новые соединения
  }
}

onApplicationBootstrap — пост-инициализация

@Injectable()
export class SchedulerService implements OnApplicationBootstrap {
  constructor(private schedulerRegistry: SchedulerRegistry) {}

  onApplicationBootstrap() {
    // Безопасно запускать cron после инициализации всех модулей
    const job = new CronJob('0 */6 * * *', () => this.runSync());
    this.schedulerRegistry.addCronJob('data-sync', job);
    job.start();
  }
}

Подводные камни

  • app.enableShutdownHooks() обязателен — без него OnApplicationShutdown не вызывается при SIGTERM, и процесс завершается мгновенно без очистки.
  • В Lambda / serverless окружениях onApplicationShutdown не вызывается при cold start timeouts — используйте try/finally в обработчиках.
  • onModuleInit выполняется синхронно в порядке инициализации модулей; долгая операция блокирует запуск приложения — добавьте timeout или retry.
  • Если onModuleInit бросает исключение, приложение не запустится; оберните нестабильные операции в try/catch с корректным логированием.
  • В тестах (Test.createTestingModule) lifecycle hooks вызываются только после явного вызова moduleRef.init() — без него onModuleInit не выполнится.
  • Порядок вызова shutdown hooks недетерминирован между разными сервисами; не полагайтесь на то, что один сервис завершится раньше другого.
  • onModuleDestroy vs onApplicationShutdown: первый вызывается при уничтожении конкретного модуля, второй — всего приложения; для финального освобождения ресурсов используйте shutdown.

Common mistakes

  • Отвечать определением без production-сценария.
  • Не называть runtime boundary, security boundary или failure mode.
  • Игнорировать версию API, observability и тестовую проверку.

What the interviewer is testing

  • Объясняет механизм своими словами и без выдуманных API.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics