Два вопроса встают в первый же день работы с любым сервисом: откуда брать настройки и как правильно открывать и закрывать ресурсы (соединения с базой, очереди, кэш). Разберём, как 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 — подключение базы данных, которую тоже нужно корректно закрывать при остановке.