Тема 8. Провайдери, сервіси та модульна система NestJS

Lifecycle Hooks: хуки життєвого циклу

onModuleInit, onApplicationBootstrap, onModuleDestroy, onApplicationShutdown

Lifecycle Hooks: хуки життєвого циклу

Короткий зміст

  • Життєвий цикл застосунку NestJS
  • Інтерфейс OnModuleInit: виконання після ініціалізації модуля
  • Інтерфейс OnApplicationBootstrap: виконання після запуску всього застосунку
  • Інтерфейс OnModuleDestroy: очищення перед знищенням модуля
  • Інтерфейс BeforeApplicationShutdown: перед завершенням застосунку
  • Інтерфейс OnApplicationShutdown: при завершенні застосунку
  • Порядок виконання hooks
  • Сценарії використання: підключення до БД, закриття з'єднань
  • Приклад: ініціалізація кешу в onModuleInit
  • Graceful shutdown: коректне завершення з очищенням ресурсів

Життєвий цикл застосунку NestJS

Кожен застосунок NestJS проходить через чітко визначений життєвий цикл (lifecycle) від запуску до завершення. NestJS надає набір lifecycle hooks (хуків життєвого циклу) — спеціальних методів, які викликаються на різних етапах життя застосунку, модулів та провайдерів.

Lifecycle hooks дозволяють виконувати код у критичні моменти:

  • Ініціалізація: підключення до БД, завантаження конфігурації, прогрівання кешу
  • Запуск: реєстрація слухачів подій, старт фонових процесів
  • Завершення: закриття з'єднань, збереження стану, очищення ресурсів

🎯 Мета лекції

  • Зрозуміти фази життєвого циклу застосунку NestJS
  • Опанувати всі доступні lifecycle hooks та їх призначення
  • Навчитися коректно ініціалізувати ресурси при запуску
  • Реалізувати graceful shutdown для чистого завершення
  • Застосовувати best practices для роботи з lifecycle hooks

🔑 Ключові терміни

  • Lifecycle Hook — метод, що викликається на певному етапі життєвого циклу
  • Initialization Phase — фаза ініціалізації модулів та провайдерів
  • Bootstrap Phase — фаза запуску застосунку
  • Termination Phase — фаза завершення застосунку
  • Graceful Shutdown — коректне завершення з очищенням ресурсів

Фази життєвого циклу та порядок виконання hooks

Loading diagram...
flowchart TD
    A[NestFactory.create] --> B[Ініціалізація модулів]
    B --> C[onModuleInit<br/>для всіх провайдерів]
    C --> D[onApplicationBootstrap<br/>для всіх провайдерів]
    D --> E[app.listen<br/>Застосунок працює]
    
    E --> F[SIGTERM / app.close]
    F --> G[enableShutdownHooks<br/>activated?]
    G -->|Так| H[onModuleDestroy<br/>для всіх провайдерів]
    G -->|Ні| L[Негайне завершення]
    H --> I[beforeApplicationShutdown<br/>для всіх провайдерів]
    I --> J[Закриття HTTP-сервера]
    J --> K[onApplicationShutdown<br/>для всіх провайдерів]
    K --> L[Процес завершено]
    
    style A fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style E fill:#10b981,stroke:#059669,color:#ffffff
    style L fill:#64748b,stroke:#334155,color:#ffffff

Детальна послідовність lifecycle hooks

Фаза 1: Ініціалізація (Initialization)

onModuleInit() викликається для кожного провайдера та контролера після створення всіх залежностей модуля, але до запуску застосунку.

Фаза 2: Завантаження (Bootstrap)

onApplicationBootstrap() викликається після завершення ініціалізації всіх модулів, але до прослуховування порту HTTP-сервером.

Фаза 3: Робота (Running)

Застосунок обробляє запити. Lifecycle hooks не викликаються.

Фаза 4: Підготовка до завершення (Pre-Termination)

onModuleDestroy() викликається після отримання сигналу завершення (SIGTERM) або виклику app.close().

Фаза 5: Завершення HTTP-сервера

NestJS закриває HTTP-сервер та завершує обробку активних з'єднань.

Фаза 6: Фінальне очищення (Shutdown)

beforeApplicationShutdown() викликається перед остаточним завершенням процесу.

onApplicationShutdown() викликається безпосередньо перед завершенням Node.js процесу.


OnModuleInit: ініціалізація після створення модуля

Інтерфейс OnModuleInit визначає метод onModuleInit(), який викликається після того, як всі залежності модуля створені, але до запуску застосунку.

Приклад: підключення до бази даних

// database/database.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Pool } from 'pg';

@Injectable()
export class DatabaseService implements OnModuleInit {
  private pool: Pool;

  async onModuleInit() {
    console.log('DatabaseService: Ініціалізація підключення до БД...');
    
    this.pool = new Pool({
      host: process.env.DB_HOST,
      port: parseInt(process.env.DB_PORT, 10),
      user: process.env.DB_USER,
      password: process.env.DB_PASSWORD,
      database: process.env.DB_NAME,
      max: 20
    });

    // Перевірка з'єднання
    try {
      const client = await this.pool.connect();
      console.log('✓ Підключення до БД встановлено');
      client.release();
    } catch (error) {
      console.error('✗ Помилка підключення до БД:', error.message);
      throw error;
    }
  }

  async query(text: string, params?: any[]) {
    return this.pool.query(text, params);
  }
}

Приклад: прогрівання кешу

// cache/cache.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';

@Injectable()
export class CacheService implements OnModuleInit {
  private cache = new Map<string, any>();

  async onModuleInit() {
    console.log('CacheService: Прогрівання кешу...');
    
    // Завантаження популярних даних у кеш
    await this.preloadPopularData();
    
    console.log(`✓ Кеш прогрітий: ${this.cache.size} записів`);
  }

  private async preloadPopularData() {
    // Симуляція завантаження з БД
    const popularItems = [
      { key: 'config:app_name', value: 'My App' },
      { key: 'config:api_version', value: 'v1.0.0' },
      { key: 'config:max_requests', value: 1000 }
    ];

    for (const item of popularItems) {
      this.cache.set(item.key, item.value);
    }
  }

  get(key: string): any {
    return this.cache.get(key);
  }

  set(key: string, value: any): void {
    this.cache.set(key, value);
  }
}
onModuleInit() може бути асинхронним. NestJS чекатиме на завершення Promise перед продовженням ініціалізації наступних модулів.

OnApplicationBootstrap: виконання після запуску застосунку

Інтерфейс OnApplicationBootstrap визначає метод onApplicationBootstrap(), який викликається після завершення ініціалізації всіх модулів, безпосередньо перед прослуховуванням порту.

Приклад: реєстрація фонових задач

// scheduler/scheduler.service.ts
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';

@Injectable()
export class SchedulerService implements OnApplicationBootstrap {
  private intervals: NodeJS.Timeout[] = [];

  onApplicationBootstrap() {
    console.log('SchedulerService: Запуск фонових задач...');
    
    // Щохвилинна задача
    this.intervals.push(
      setInterval(() => this.cleanupExpiredSessions(), 60000)
    );

    // Щогодинна задача
    this.intervals.push(
      setInterval(() => this.generateReports(), 3600000)
    );

    console.log('✓ Фонові задачі запущено');
  }

  private cleanupExpiredSessions() {
    console.log('[Cron] Очищення застарілих сесій');
    // Логіка очищення
  }

  private generateReports() {
    console.log('[Cron] Генерація звітів');
    // Логіка генерації звітів
  }
}

Приклад: підписка на події WebSocket

// notifications/notifications.service.ts
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';

@Injectable()
export class NotificationsService implements OnApplicationBootstrap {
  constructor(private readonly eventEmitter: EventEmitter2) {}

  onApplicationBootstrap() {
    console.log('NotificationsService: Підписка на події...');
    
    // Підписка на події користувачів
    this.eventEmitter.on('user.registered', (data) => {
      this.sendWelcomeEmail(data.email);
    });

    this.eventEmitter.on('order.completed', (data) => {
      this.sendOrderConfirmation(data.userId, data.orderId);
    });

    console.log('✓ Підписки на події налаштовано');
  }

  private sendWelcomeEmail(email: string) {
    console.log(`Відправка вітального email на ${email}`);
  }

  private sendOrderConfirmation(userId: number, orderId: number) {
    console.log(`Підтвердження замовлення #${orderId} для користувача #${userId}`);
  }
}
Використовуйте onApplicationBootstrap() для задач, що потребують повністю ініціалізованого застосунку, наприклад, запуск WebSocket-серверів, підписка на зовнішні події або реєстрація у service discovery.

OnModuleDestroy: очищення перед знищенням модуля

Інтерфейс OnModuleDestroy визначає метод onModuleDestroy(), який викликається перед знищенням модуля при завершенні застосунку.

Приклад: закриття підключення до БД

// database/database.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { Pool } from 'pg';

@Injectable()
export class DatabaseService implements OnModuleInit, OnModuleDestroy {
  private pool: Pool;

  async onModuleInit() {
    this.pool = new Pool({
      host: process.env.DB_HOST,
      port: parseInt(process.env.DB_PORT, 10),
      database: process.env.DB_NAME
    });
    console.log('✓ Підключення до БД встановлено');
  }

  async onModuleDestroy() {
    console.log('DatabaseService: Закриття підключення до БД...');
    
    await this.pool.end();
    
    console.log('✓ Підключення до БД закрито');
  }

  async query(text: string, params?: any[]) {
    return this.pool.query(text, params);
  }
}

Приклад: зупинка фонових задач

// scheduler/scheduler.service.ts
import { Injectable, OnApplicationBootstrap, OnModuleDestroy } from '@nestjs/common';

@Injectable()
export class SchedulerService implements OnApplicationBootstrap, OnModuleDestroy {
  private intervals: NodeJS.Timeout[] = [];

  onApplicationBootstrap() {
    this.intervals.push(
      setInterval(() => this.cleanupExpiredSessions(), 60000)
    );
    console.log('✓ Фонові задачі запущено');
  }

  onModuleDestroy() {
    console.log('SchedulerService: Зупинка фонових задач...');
    
    // Очищення всіх інтервалів
    this.intervals.forEach(interval => clearInterval(interval));
    this.intervals = [];
    
    console.log('✓ Фонові задачі зупинено');
  }

  private cleanupExpiredSessions() {
    // Логіка очищення
  }
}
onModuleDestroy() викликається тільки якщо увімкнено graceful shutdown через app.enableShutdownHooks(). Без цього hook не спрацює!

BeforeApplicationShutdown та OnApplicationShutdown

Ці hooks виконуються на фінальних етапах завершення застосунку:

BeforeApplicationShutdown

Викликається після onModuleDestroy(), але до остаточного завершення процесу. Отримує сигнал завершення як параметр.

// metrics/metrics.service.ts
import { Injectable, BeforeApplicationShutdown } from '@nestjs/common';

@Injectable()
export class MetricsService implements BeforeApplicationShutdown {
  private metrics = {
    requestsProcessed: 0,
    errorsCount: 0
  };

  beforeApplicationShutdown(signal?: string) {
    console.log(`MetricsService: Отримано сигнал завершення: ${signal}`);
    console.log('Збереження метрик перед завершенням...');
    
    // Збереження метрик у файл або БД
    this.saveMetricsToFile();
    
    console.log('✓ Метрики збережено');
  }

  private saveMetricsToFile() {
    const fs = require('fs');
    fs.writeFileSync(
      'metrics.json',
      JSON.stringify(this.metrics, null, 2)
    );
  }

  incrementRequests() {
    this.metrics.requestsProcessed++;
  }

  incrementErrors() {
    this.metrics.errorsCount++;
  }
}

OnApplicationShutdown

Викликається безпосередньо перед завершенням Node.js процесу. Останній шанс виконати критичні операції.

// logger/logger.service.ts
import { Injectable, OnApplicationShutdown } from '@nestjs/common';

@Injectable()
export class LoggerService implements OnApplicationShutdown {
  private logBuffer: string[] = [];

  onApplicationShutdown(signal?: string) {
    console.log(`LoggerService: Фінальна синхронізація логів (${signal})`);
    
    // Скидання буфера логів
    this.flushLogs();
    
    console.log('✓ Логи синхронізовано');
  }

  log(message: string) {
    this.logBuffer.push(`[${new Date().toISOString()}] ${message}`);
  }

  private flushLogs() {
    // Запис усіх буферизованих логів
    this.logBuffer.forEach(log => console.log(log));
    this.logBuffer = [];
  }
}

Увімкнення Graceful Shutdown

За замовчуванням NestJS не викликає hooks завершення (onModuleDestroy, beforeApplicationShutdown, onApplicationShutdown). Щоб увімкнути graceful shutdown, потрібно явно активувати його:

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // ✅ Увімкнення graceful shutdown
  app.enableShutdownHooks();

  await app.listen(3000);
  console.log('Застосунок запущено на http://localhost:3000');
}

bootstrap();

Тепер при отриманні сигналів SIGTERM або SIGINT (Ctrl+C) NestJS коректно завершить роботу:

Graceful Shutdown
$ npm run start:prod
INFO Застосунок запущено на http://localhost:3000
✓ DatabaseService підключено до БД
✓ CacheService прогрітий: 150 записів
✓ SchedulerService фонові задачі запущено
[Користувач натискає Ctrl+C]
WARN Отримано сигнал SIGINT. Завершення роботи...
INFO SchedulerService: Зупинка фонових задач...
✓ Фонові задачі зупинено
INFO DatabaseService: Закриття підключення до БД...
✓ Підключення до БД закрито
INFO MetricsService: Збереження метрик...
✓ Метрики збережено
Застосунок коректно завершено
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF
autonumber

participant "SIGTERM" as Signal #FEF3C7
participant "NestJS" as Nest #DBEAFE
participant "DatabaseService" as DB #DCFCE7
participant "SchedulerService" as Scheduler #E0E7FF
participant "MetricsService" as Metrics #F3E8FF

Signal -> Nest : SIGTERM отримано
Nest -> DB : onModuleDestroy()
DB -> DB : Закриття пулу з'єднань
DB --> Nest : Завершено

Nest -> Scheduler : onModuleDestroy()
Scheduler -> Scheduler : clearInterval(tasks)
Scheduler --> Nest : Завершено

Nest -> Metrics : beforeApplicationShutdown(signal)
Metrics -> Metrics : Збереження метрик
Metrics --> Nest : Завершено

Nest -> Nest : Закриття HTTP-сервера

Nest -> DB : onApplicationShutdown(signal)
Nest -> Scheduler : onApplicationShutdown(signal)
Nest -> Metrics : onApplicationShutdown(signal)

Nest -> Signal : process.exit(0)

note over Nest
  Graceful shutdown:
  всі ресурси коректно
  звільнено перед виходом
end note

@enduml

Повний приклад: сервіс з lifecycle hooks

// redis/redis.service.ts
import {
  Injectable,
  OnModuleInit,
  OnApplicationBootstrap,
  OnModuleDestroy,
  BeforeApplicationShutdown,
  OnApplicationShutdown
} from '@nestjs/common';
import { createClient, RedisClientType } from 'redis';

@Injectable()
export class RedisService
  implements
    OnModuleInit,
    OnApplicationBootstrap,
    OnModuleDestroy,
    BeforeApplicationShutdown,
    OnApplicationShutdown
{
  private client: RedisClientType;
  private connectionStatus: 'connecting' | 'connected' | 'disconnecting' | 'disconnected' = 'disconnected';

  // 1. Ініціалізація після створення модуля
  async onModuleInit() {
    console.log('[1] RedisService.onModuleInit: Підключення до Redis...');
    
    this.connectionStatus = 'connecting';
    this.client = createClient({
      url: process.env.REDIS_URL || 'redis://localhost:6379'
    });

    this.client.on('error', (err) => console.error('Redis Error:', err));

    await this.client.connect();
    this.connectionStatus = 'connected';
    
    console.log('✓ Redis підключено');
  }

  // 2. Виконання після запуску застосунку
  onApplicationBootstrap() {
    console.log('[2] RedisService.onApplicationBootstrap: Перевірка з'єднання...');
    
    // Запис тестового ключа
    this.client.set('app:status', 'running', { EX: 3600 });
    
    console.log('✓ Redis готовий до роботи');
  }

  // 3. Очищення перед знищенням модуля
  async onModuleDestroy() {
    console.log('[3] RedisService.onModuleDestroy: Початок відключення...');
    
    this.connectionStatus = 'disconnecting';
    
    // Видалення тестового ключа
    await this.client.del('app:status');
  }

  // 4. Перед завершенням застосунку
  async beforeApplicationShutdown(signal?: string) {
    console.log(`[4] RedisService.beforeApplicationShutdown: Сигнал ${signal}`);
    
    // Збереження критичних даних перед відключенням
    const pendingWrites = await this.getPendingWrites();
    console.log(`Очікуються записи: ${pendingWrites}`);
  }

  // 5. При завершенні застосунку
  async onApplicationShutdown(signal?: string) {
    console.log(`[5] RedisService.onApplicationShutdown: Закриття з'єднання (${signal})`);
    
    await this.client.quit();
    this.connectionStatus = 'disconnected';
    
    console.log('✓ Redis відключено');
  }

  // Публічний API сервісу
  async get(key: string): Promise<string | null> {
    if (this.connectionStatus !== 'connected') {
      throw new Error('Redis not connected');
    }
    return this.client.get(key);
  }

  async set(key: string, value: string, ttl?: number): Promise<void> {
    if (this.connectionStatus !== 'connected') {
      throw new Error('Redis not connected');
    }
    await this.client.set(key, value, ttl ? { EX: ttl } : undefined);
  }

  private async getPendingWrites(): Promise<number> {
    // Логіка підрахунку незавершених записів
    return 0;
  }
}

Best Practices для lifecycle hooks

⚡ Швидкість виконання

Принцип: Hooks мають виконуватися швидко

Рекомендації:

  • Уникайте довгих синхронних операцій у hooks
  • Використовуйте таймаути для зовнішніх викликів
  • Логуйте час виконання для моніторингу

🔒 Безпека даних

Принцип: Критичні дані мають бути збережені перед завершенням

Рекомендації:

  • Скидайте буфери у beforeApplicationShutdown()
  • Завершуйте активні транзакції перед відключенням
  • Перевіряйте статус збереження перед виходом

📝 Логування

Принцип: Логуйте всі етапи життєвого циклу

Рекомендації:

  • Додавайте логи у всі lifecycle hooks
  • Вказуйте назву сервісу та етап
  • Логуйте помилки з контекстом

⏱️ Graceful Shutdown

Принцип: Завжди увімкнюйте graceful shutdown у продакшні

Рекомендації:

  • Викликайте app.enableShutdownHooks() у main.ts
  • Встановлюйте таймаути для завершення
  • Тестуйте поведінку при SIGTERM/SIGINT

Висновки

Lifecycle hooks є потужним механізмом для контролю поведінки застосунку на різних етапах життєвого циклу. Ключові принципи:

  • Ініціалізація: onModuleInit() для підготовки ресурсів, onApplicationBootstrap() для запуску фонових процесів
  • Завершення: onModuleDestroy() для очищення, beforeApplicationShutdown() для збереження даних, onApplicationShutdown() для фінальних операцій
  • Graceful Shutdown: обов'язкове увімкнення через app.enableShutdownHooks() для коректного завершення
  • Порядок: hooks викликаються у чіткій послідовності для всіх модулів та провайдерів
Використовуйте lifecycle hooks для критичних операцій ініціалізації та очищення. Це забезпечить надійну роботу застосунку та коректне звільнення ресурсів при завершенні.

У наступній лекції ми розглянемо ModuleRef — спеціальний клас для динамічного доступу до провайдерів у runtime.

Copyright © 2026