← назад к разделу

Два вопроса встают в первый же день работы с любым сервисом: откуда брать настройки и как правильно открывать и закрывать ресурсы (соединения с базой, очереди, кэш). Разберём, как NestJS решает оба.

Проблема с process.env напрямую

Поначалу кажется удобным читать настройки прямо там, где они нужны:

const secret = process.env.JWT_SECRET; // где-то в глубине сервиса

Через месяц это превращается в проблему: непонятно, какие переменные вообще нужны приложению, ошибки в названиях переменных всплывают не на старте, а в момент конкретного запроса, и тестировать сервис с подменёнными настройками неудобно.

ConfigModule решает это: все настройки читаются в одном месте, проверяются при запуске и отдаются через типизированный ConfigService.

ConfigModule: настройки в одном месте

Устанавливается отдельным пакетом:

npm install @nestjs/config

Подключается в корневом модуле:

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
  ],
})
export class AppModule {}

isGlobal: true означает, что ConfigService доступен во всех модулях без дополнительных импортов — подключили один раз и пользуетесь везде.

ConfigModule автоматически читает файл .env в корне проекта и объединяет его с переменными окружения. Переменные окружения имеют приоритет над .env — это удобно: локально работаете с .env, а на сервере задаёте реальные значения через системное окружение.

Как получать значения

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class JwtConfig {
  constructor(private readonly config: ConfigService) {}

  get secret(): string {
    return this.config.getOrThrow<string>('JWT_SECRET');
  }

  get expiresIn(): string {
    return this.config.get<string>('JWT_EXPIRES_IN', '1h');
  }
}

Два метода с разной семантикой:

  • getOrThrow — если переменной нет, выбрасывает исключение. Используйте для обязательных настроек: лучше упасть на старте с понятной ошибкой, чем гонять undefined по коду.
  • get со вторым аргументом — значение по умолчанию, для опциональных настроек.

Проверка конфигурации на старте

Ещё надёжнее — валидировать весь конфиг при запуске через схему. Для этого обычно используют zod или class-validator с class-transformer:

import { z } from 'zod';

const envSchema = z.object({
  JWT_SECRET: z.string().min(32),
  DATABASE_URL: z.string().url(),
  PORT: z.coerce.number().default(3000),
});

export function validate(config: Record<string, unknown>) {
  const result = envSchema.safeParse(config);
  if (!result.success) {
    throw new Error(`Ошибка конфигурации: ${result.error.message}`);
  }
  return result.data;
}
ConfigModule.forRoot({ isGlobal: true, validate })

Если обязательной переменной нет или она не того формата — приложение падает сразу при старте с понятным сообщением. Это лучше, чем загадочный сбой в рантайме через час после запуска.

Lifecycle-хуки: открываем и закрываем ресурсы правильно

Допустим, ваш сервис подключается к очереди сообщений. Нужно установить соединение при запуске и закрыть его при остановке. Делать это в конструкторе неудобно — конструктор не поддерживает async. Для этого есть lifecycle-хуки.

Провайдер реализует нужный интерфейс:

import { Injectable, OnModuleInit, OnApplicationShutdown } from '@nestjs/common';

@Injectable()
export class QueueConsumer implements OnModuleInit, OnApplicationShutdown {
  async onModuleInit() {
    await this.connect(); // соединяемся, когда модуль собран
  }

  async onApplicationShutdown(signal?: string) {
    await this.disconnect(); // закрываем при остановке
  }
}

NestJS вызывает эти методы автоматически в нужный момент — вам не нужно думать об этом в коде запуска.

Основные хуки по порядку:

ХукКогда срабатывает
onModuleInitмодуль собран, зависимости внедрены
onApplicationBootstrapвсё приложение поднято, сервер слушает
onModuleDestroyначало остановки
onApplicationShutdownпроцесс получил сигнал завершения

Принцип прост: что открыли в onModuleInit — закрываем в onApplicationShutdown. Симметрия помогает не забыть очистку.

Graceful shutdown: корректная остановка

Хуки остановки срабатывают только если явно включить обработку сигналов операционной системы:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableShutdownHooks();
  await app.listen(3000);
}
bootstrap();

Без enableShutdownHooks() при команде остановки процесс завершится немедленно, не вызвав onApplicationShutdown — соединения останутся незакрытыми, незавершённые запросы потеряются.

Это важно в контейнерном окружении: когда Kubernetes останавливает под, он посылает SIGTERM и даёт время на завершение. Если enableShutdownHooks включён, сервис успевает завершить обработку текущих запросов и закрыть соединения до того, как его принудительно прибьют.

Bootstrap: точка сборки приложения

main.ts — файл, где приложение создаётся и получает глобальные настройки:

import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

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

  app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
  app.useGlobalFilters(new AllExceptionsFilter());
  app.enableShutdownHooks();

  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

Здесь в одном месте видно весь «край» сервиса: как обрабатываются входящие данные, как форматируются ошибки, как ведёт себя при остановке. Если понадобится что-то поменять глобально — знаете, куда идти.

Коротко

  • Читайте настройки через ConfigModule + ConfigService, а не напрямую из process.env — всё в одном месте, легче тестировать.
  • isGlobal: true — подключили один раз, доступно везде.
  • Используйте validate для проверки конфига на старте: лучше упасть сразу, чем в рантайме.
  • getOrThrow для обязательных переменных, get с дефолтом — для опциональных.
  • Открывайте ресурсы в onModuleInit, закрывайте в onApplicationShutdown — симметрия не даёт забыть очистку.
  • enableShutdownHooks() обязателен — без него хуки остановки не сработают.

Что почитать дальше

  • Валидация и pipes — как устроена глобальная валидация входных данных.
  • Exception filters — единый формат ошибок для всего API.
  • Персистентность: TypeORM — подключение базы данных, которую тоже нужно корректно закрывать при остановке.