Пошук уроків, статей та іншого контенту
Розберете послідовність створення, ініціалізації та знищення модулів і залежностей у NestJS.
Життєвий цикл NestJS — це послідовність етапів, через які проходять модулі, провайдери та застосунок:
NestJS читає метадані модулів.
Створює екземпляри модулів і залежностей.
Викликає методи ініціалізації.
Запускає застосунок.
Під час завершення роботи викликає методи знищення.
Цей механізм дає змогу виконувати код у правильний момент:
підключатися до зовнішніх сервісів;
перевіряти конфігурацію;
реєструвати ресурси;
запускати фонові процеси;
коректно закривати з’єднання.
Життєвий цикл стосується не лише класів модулів. Спеціальні lifecycle hooks можуть реалізовувати модулі, провайдери та контролери.
Спрощено запуск NestJS має такий вигляд:
Викликається NestFactory.create().
NestJS аналізує AppModule та імпортовані модулі.
Створюються екземпляри модулів, провайдерів і контролерів.
NestJS викликає onModuleInit().
NestJS викликає onApplicationBootstrap()
Застосунок починає приймати запити після app.listen().
У реальному застосунку NestJS будує граф залежностей. Якщо один провайдер залежить від іншого, спочатку має бути створена його залежність.
Конструктор викликається під час створення екземпляра класу:
@Injectable()
export class DatabaseService {
constructor() {
console.log('Екземпляр DatabaseService створено');
}
}Конструктор варто використовувати для отримання залежностей:
@Injectable()
export class UsersService {
constructor(
private readonly databaseService: DatabaseService,
) {}
}Не слід використовувати конструктор для складної асинхронної ініціалізації. Конструктор не може бути async, тому для підключення до бази даних або зовнішнього сервісу краще використовувати onModuleInit().
NestJS надає інтерфейси, які описують методи життєвого циклу.
Основні hooks:
OnModuleInit — після ініціалізації модуля;
OnApplicationBootstrap — після ініціалізації всіх модулів;
OnModuleDestroy — під час знищення модуля;
BeforeApplicationShutdown — перед завершенням застосунку;
OnApplicationShutdown — під час завершення застосунку.
Інтерфейси не є обов’язковими для реалізації з технічного погляду, але їх потрібно використовувати явно. Це робить код зрозумілішим і допомагає TypeScript перевіряти назви методів.
OnModuleInitOnModuleInit містить метод onModuleInit(). NestJS викликає його після створення всіх залежностей, необхідних для відповідного модуля.
import {
Injectable,
OnModuleInit,
} from '@nestjs/common';
@Injectable()
export class CacheService implements OnModuleInit {
onModuleInit() {
console.log('Кеш готовий до роботи');
}
}Метод може бути асинхронним:
import {
Injectable,
OnModuleInit,
} from '@nestjs/common';
@Injectable()
export class ExternalApiService implements OnModuleInit {
async onModuleInit(): Promise<void> {
await this.connect();
console.log('Зʼєднання із зовнішнім API встановлено');
}
private async connect(): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, 100));
}
}NestJS дочекається завершення Promise, повернутого з onModuleInit().
OnModuleInitЦей hook підходить для:
встановлення з’єднання з базою даних;
завантаження початкових даних;
перевірки доступності залежностей;
підготовки кешу;
запуску внутрішнього стану провайдера.
Якщо провайдер належить модулю, його onModuleInit() виконується в межах ініціалізації цього модуля.
OnApplicationBootstrapOnApplicationBootstrap містить метод onApplicationBootstrap(). Він викликається після того, як усі модулі застосунку завершили свою ініціалізацію.
import {
Injectable,
OnApplicationBootstrap,
} from '@nestjs/common';
@Injectable()
export class ReportsService implements OnApplicationBootstrap {
onApplicationBootstrap() {
console.log('Усі модулі готові, можна запускати звіти');
}
}Цей hook корисний, коли код має запускатися лише після готовності всього застосунку.
Наприклад, сервіс може використовувати кілька інших модулів:
@Injectable()
export class SearchService implements OnApplicationBootstrap {
async onApplicationBootstrap(): Promise<void> {
await this.buildSearchIndex();
}
private async buildSearchIndex(): Promise<void> {
// Побудова індексу після готовності всіх модулів
}
}OnModuleInit і OnApplicationBootstrapOnModuleInit призначений для підготовки конкретного модуля або провайдера.
OnApplicationBootstrap виконується пізніше — коли NestJS завершив ініціалізацію всього застосунку.
Наприклад:
підключення до бази даних — OnModuleInit;
побудова індексу, якому потрібні база даних і конфігурація з інших модулів — OnApplicationBootstrap.
Застосунок може завершити роботу через:
явний виклик app.close();
сигнал операційної системи, якщо ввімкнено обробку сигналів;
завершення тестового середовища;
інший код, який закриває Nest application.
Під час завершення NestJS викликає hooks у такій послідовності:
onModuleDestroy();
beforeApplicationShutdown();
NestJS закриває сервер і ресурси;
onApplicationShutdown().
OnModuleDestroyOnModuleDestroy містить метод onModuleDestroy(). Він призначений для звільнення ресурсів конкретного модуля.
import {
Injectable,
OnModuleDestroy,
} from '@nestjs/common';
@Injectable()
export class QueueService implements OnModuleDestroy {
onModuleDestroy() {
console.log('Черга зупиняється');
}
}Асинхронний варіант:
@Injectable()
export class DatabaseService implements OnModuleDestroy {
async onModuleDestroy(): Promise<void> {
await this.disconnect();
}
private async disconnect(): Promise<void> {
// Закриття з’єднання з базою даних
}
}Типові завдання для onModuleDestroy():
закрити з’єднання;
зупинити таймери;
припинити фонові задачі;
відписатися від подій;
звільнити ресурси модуля.
BeforeApplicationShutdownBeforeApplicationShutdown викликається після onModuleDestroy() і перед фактичним завершенням застосунку.
Метод отримує сигнал завершення, якщо він був переданий операційною системою:
import {
Injectable,
BeforeApplicationShutdown,
} from '@nestjs/common';
@Injectable()
export class ShutdownService implements BeforeApplicationShutdown {
beforeApplicationShutdown(signal?: string): void {
console.log(`Застосунок завершується через сигнал: ${signal ?? 'невідомий'}`);
}
}Цей hook зручно використовувати для дій, які стосуються всього застосунку:
записати фінальний стан;
повідомити інші компоненти про завершення;
дочекатися завершення короткої операції;
виконати загальну підготовку до зупинки.
OnApplicationShutdownOnApplicationShutdown викликається під час завершення застосунку після закриття основних ресурсів NestJS.
import {
Injectable,
OnApplicationShutdown,
} from '@nestjs/common';
@Injectable()
export class MetricsService implements OnApplicationShutdown {
onApplicationShutdown(signal?: string): void {
console.log(`Метрики завершують роботу: ${signal ?? 'ручне закриття'}`);
}
}Цей hook може бути корисним для фінального очищення, яке має виконуватися на останньому етапі завершення.
За замовчуванням NestJS не завжди автоматично обробляє сигнали на кшталт SIGTERM або SIGINT.
Щоб hooks завершення викликалися під час натискання Ctrl+C або зупинки контейнера, потрібно викликати:
app.enableShutdownHooks();Повний приклад:
import {
BeforeApplicationShutdown,
Injectable,
Module,
OnApplicationBootstrap,
OnApplicationShutdown,
OnModuleDestroy,
OnModuleInit,
} from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
@Injectable()
export class DatabaseService
implements OnModuleInit, OnModuleDestroy
{
async onModuleInit(): Promise<void> {
console.log('DatabaseService: підключення до бази даних');
await new Promise((resolve) => setTimeout(resolve, 100));
console.log('DatabaseService: підключення встановлено');
}
async onModuleDestroy(): Promise<void> {
console.log('DatabaseService: закриття зʼєднання');
await new Promise((resolve) => setTimeout(resolve, 100));
console.log('DatabaseService: зʼєднання закрито');
}
}
@Injectable()
export class ApplicationLifecycleService
implements OnApplicationBootstrap, BeforeApplicationShutdown, OnApplicationShutdown
{
onApplicationBootstrap(): void {
console.log('ApplicationLifecycleService: застосунок ініціалізовано');
}
beforeApplicationShutdown(signal?: string): void {
console.log(
`ApplicationLifecycleService: підготовка до завершення (${signal ?? 'app.close()'})`,
);
}
onApplicationShutdown(signal?: string): void {
console.log(
`ApplicationLifecycleService: завершення виконано (${signal ?? 'app.close()'})`,
);
}
}
@Module({
providers: [
DatabaseService,
ApplicationLifecycleService,
],
})
export class AppModule {}
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);
// Дозволяє обробляти SIGINT і SIGTERM та викликати shutdown hooks
app.enableShutdownHooks();
await app.listen(3000);
console.log('HTTP-сервер запущено на порту 3000');
}
void bootstrap();Після запуску застосунку приблизний порядок повідомлень буде таким:
DatabaseService: підключення до бази даних
DatabaseService: підключення встановлено
ApplicationLifecycleService: застосунок ініціалізовано
HTTP-сервер запущено на порту 3000Після натискання Ctrl+C:
DatabaseService: закриття зʼєднання
DatabaseService: зʼєднання закрито
ApplicationLifecycleService: підготовка до завершення (SIGINT)
ApplicationLifecycleService: завершення виконано (SIGINT)Точний порядок між кількома провайдерами одного рівня не варто використовувати як неявний механізм координації. Якщо порядок важливий, залежності потрібно виразити через DI або створити окремий сервіс-координатор.
Розглянемо залежність між модулями:
@Module({
providers: [DatabaseService],
exports: [DatabaseService],
})
export class DatabaseModule {}
@Module({
imports: [DatabaseModule],
providers: [UsersService],
})
export class UsersModule {}UsersModule імпортує DatabaseModule, тому UsersService може отримати DatabaseService через конструктор:
@Injectable()
export class UsersService implements OnModuleInit {
constructor(
private readonly databaseService: DatabaseService,
) {}
onModuleInit(): void {
console.log('UsersService може використовувати DatabaseService');
}
}Залежності потрібно експортувати з модуля, якщо вони мають бути доступними в іншому модулі:
@Module({
providers: [DatabaseService],
exports: [DatabaseService],
})
export class DatabaseModule {}Без exports інший модуль не зможе інжектити цей провайдер лише через imports.
Під час ініціалізації NestJS намагається підготувати залежності до використання. Тому код у onModuleInit() може безпечно використовувати залежності, отримані через конструктор, якщо вони коректно зареєстровані в графі модулів.
Lifecycle hook може повертати Promise:
async onModuleInit(): Promise<void> {
await this.loadConfiguration();
}Це важливо, коли наступні етапи не повинні починатися до завершення підготовки.
Наприклад:
@Injectable()
export class ConfigurationService implements OnModuleInit {
private configuration: Record<string, string> = {};
async onModuleInit(): Promise<void> {
this.configuration = await this.loadConfiguration();
}
get(key: string): string | undefined {
return this.configuration[key];
}
private async loadConfiguration(): Promise<Record<string, string>> {
await new Promise((resolve) => setTimeout(resolve, 50));
return {
environment: 'development',
};
}
}Не варто запускати асинхронну операцію без await, якщо від її завершення залежить готовність модуля:
// Погано: NestJS не очікує завершення операції
onModuleInit(): void {
this.connect();
}Краще:
// Добре: NestJS очікує завершення підключення
async onModuleInit(): Promise<void> {
await this.connect();
}Якщо код має виконатися один раз під час запуску застосунку, не запускайте його одночасно в конструкторі та onModuleInit().
// Небажаний підхід
constructor() {
this.connect();
}
async onModuleInit() {
await this.connect();
}Виберіть один відповідний етап. Для асинхронної підготовки зазвичай використовуйте onModuleInit().
Якщо помилка в onModuleInit() означає, що застосунок не може працювати, її не слід мовчки ігнорувати:
async onModuleInit(): Promise<void> {
await this.connect();
}У такому випадку помилка ініціалізації може зупинити запуск застосунку, замість того щоб залишити його в неповністю готовому стані.
Lifecycle hooks можуть викликатися для кожного екземпляра провайдера. Якщо провайдер має область видимості TRANSIENT або REQUEST, його життєвий цикл може відрізнятися від життєвого циклу singleton-провайдера.
Для глобальної одноразової ініціалізації краще використовувати звичайний singleton-провайдер, якщо це відповідає завданню.
Не варто припускати, що два незалежні провайдери завжди виконають onModuleInit() у потрібному вам порядку:
@Injectable()
export class FirstService implements OnModuleInit {
onModuleInit(): void {
// ...
}
}
@Injectable()
export class SecondService implements OnModuleInit {
onModuleInit(): void {
// Не слід неявно покладатися на запуск після FirstService
}
}Якщо SecondService справді потребує готовності FirstService, додайте залежність:
@Injectable()
export class SecondService implements OnModuleInit {
constructor(
private readonly firstService: FirstService,
) {}
onModuleInit(): void {
this.firstService.doSomething();
}
}enableShutdownHooks()Проблема:
const app = await NestFactory.create(AppModule);
await app.listen(3000);У такому випадку hooks завершення можуть не викликатися під час SIGINT або SIGTERM.
Виправлення:
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks();
await app.listen(3000);awaitПроблема:
onModuleInit(): void {
this.loadData();
}Застосунок може перейти до наступного етапу до завершення loadData().
Виправлення:
async onModuleInit(): Promise<void> {
await this.loadData();
}Проблема — виконувати складну ініціалізацію в конструкторі:
constructor() {
// Тут складно коректно обробити асинхронну помилку
}Краще перенести її в onModuleInit().
Lifecycle hooks викликаються лише для екземплярів, які створює контейнер NestJS.
Якщо клас не зареєстрований у providers, NestJS не створить його екземпляр:
@Module({
providers: [DatabaseService],
})
export class AppModule {}Якщо провайдер потрібен іншому модулю, його потрібно експортувати:
@Module({
providers: [DatabaseService],
exports: [DatabaseService],
})
export class DatabaseModule {}NestJS спочатку створює модулі та їхні залежності, потім ініціалізує застосунок.
OnModuleInit використовується для підготовки конкретного модуля або провайдера.
OnApplicationBootstrap викликається після ініціалізації всіх модулів.
OnModuleDestroy призначений для звільнення ресурсів модуля.
BeforeApplicationShutdown виконується перед завершенням застосунку.
OnApplicationShutdown виконується на фінальному етапі завершення.
Для обробки SIGINT і SIGTERM потрібно викликати app.enableShutdownHooks().
Асинхронні lifecycle hooks мають повертати Promise, який потрібно очікувати через await.
Порядок роботи незалежних провайдерів не слід використовувати як неявний механізм координації.