Nx NestJs — простой способ получения, автозаполнения и проверки переменных окружения

Приветствую, сегодня мы поговорим о переменных окружения в NestJs, а именно об использовании Nx.

Как вы, возможно, знаете, Nx автогенерирует для нас много шаблонного кода. То же самое он делает и с файлами окружения.
После создания нового проекта вы можете увидеть, что вас ждут 2 файла environment и environment.prod.ts.
Эти файлы в основном одинаковы, файл .prod заменит другой при сборке, если установлен флаг production.
Всю эту магию можно увидеть внутри project.json.

Если бы мы могли просто установить там все, то эта статья была бы бессмысленной. Обычно мы хотим, чтобы секреты приложения были… секретными. Для локального dev-окружения, возможно, и хорошо сохранить в репо свой конфиг, но для продакшена это точно не подходит.

Поэтому, чтобы упростить многие вещи, я предлагаю смешанный подход. Для локальной среды мы задаем все в environment.ts, а для production (и других) мы задаем только несекретные вещи в environment.prod.ts, а остальное — переменные окружения.

Приступаем к работе!

Конфигурация

Во-первых, нам понадобится модуль ConfigModule, доступный в приложении, поэтому выполните команду npm install --save @nestjs/config class-validator class-transformer.

Затем в папке environment (или другой) вы можете создать env-config.ts, который будет содержать

/** Environment variables take precedence */
export function getEnvConfig() {
  const envVariables = parseEnvVariables();
  return mergeObject(env, envVariables);
}

function parseEnvVariables() {
  const envVariables: DeepPartial<IEnvironment> = {};
  const env = process.env;
  if (env) {
    if (env.PRODUCTION) envVariables.production = env.PRODUCTION === 'true';
    if (env.PORT) envVariables.port = parseInt(env.PORT, 10);
    if (env.BASE_URL) envVariables.baseUrl = env.BASE_URL;
    if (env.GLOBAL_API_PREFIX) envVariables.globalApiPrefix = env.GLOBAL_API_PREFIX;

    envVariables.database = {};
    if (env.DATABASE_HOST) envVariables.database.host = env.DATABASE_HOST;
    if (env.DATABASE_PORT) envVariables.database.port = parseInt(env.DATABASE_PORT, 10);
    if (env.DATABASE_DATABASE) envVariables.database.database = env.DATABASE_DATABASE;
    if (env.DATABASE_USERNAME) envVariables.database.username = env.DATABASE_USERNAME;
    if (env.DATABASE_PASSWORD) envVariables.database.password = env.DATABASE_PASSWORD;
  }
  return envVariables;
}


function mergeObject(obj1: Record<string, any>, obj2: Record<string, any>) {
  for (const key in obj2) {
    if (obj2.hasOwnProperty(key)) {
      if (typeof obj2[key] === 'object') {
        mergeObject(obj1[key], obj2[key]);
      } else {
        obj1[key] = obj2[key];
      }
    }
  }
  return obj1;
}

type DeepPartial<T> = T extends object
  ? {
      [P in keyof T]?: DeepPartial<T[P]>;
    }
  : T;
Вход в полноэкранный режим Выход из полноэкранного режима

Поясним, функция getEnvConfig будет вызываться ConfigModule при запуске и получит переменные окружения и объект окружения из environment.ts. Затем мы объединим все свойства вместе и вернем объект. Переменные окружения имеют приоритет при объединении.

Возможно, вы заметили и некоторые интерфейсы. Лучшая типизация — это всегда хорошо, поэтому нам нужно создать env.interface.ts в той же папке, содержащей

export interface IEnvironment {
  production: boolean;
  port: number;
  baseUrl: string;
  globalApiPrefix: string;
  database: IDatabaseEnvironment;
}

export interface IDatabaseEnvironment {
  host: string;
  port: number;
  database: string;
  username: string;
  password: string;
}
Вход в полноэкранный режим Выход из полноэкранного режима

На этом этапе мы почти закончили.
Все, что нам еще нужно сделать, это добавить вышеуказанный интерфейс в environment.ts и environment.prod.ts.
Мои файлы выглядят следующим образом:

// environment.ts
import { IEnvironment } from './env.interface';

export const env: Partial<IEnvironment> = {
  production: false,
  globalApiPrefix: 'api',
  port: 3333,
  baseUrl: 'http://localhost',
  database: {
    host: 'localhost',
    port: 5432,
    database: 'postgres',
    username: 'postgres',
    password: 'password',
  },
};


// environment.prod.ts
export const env: Partial<IEnvironment> = {
  production: true,
};
Вход в полноэкранный режим Выход из полноэкранного режима

Осталось зарегистрировать ConfigModule, поэтому откройте app.module.ts и добавьте

 ConfigModule.forRoot({
      load: [getEnvConfig],
      isGlobal: true,
      cache: true,
    }),
Войти в полноэкранный режим Выйти из полноэкранного режима

Это должно помочь вам начать работу. Теперь поговорим о валидации.

Валидация

Один из способов — это Joi, но он не рекомендуется для пользователей Node выше v17.
Лично я использовал пользовательскую валидацию.
Создайте в той же папке environment еще один файл env-validator.

import { plainToClass } from 'class-transformer';
import { IsBoolean, IsNumber, IsObject, IsString, validateSync } from 'class-validator';

import { getEnvConfig } from './env-config';
import { IDatabaseEnvironment, IEnvironment } from './env.interface';

class DatabaseEnvironment implements IDatabaseEnvironment {
  @IsString()
  host: string;
  @IsNumber()
  port: number;
  @IsString()
  database: string;
  @IsString()
  username: string;
  @IsString()
  password: string;
}
class Environment implements IEnvironment {
  @IsBoolean()
  production: boolean;

  @IsNumber()
  port: number;

  @IsString()
  baseUrl: string;

  @IsObject()
  database: DatabaseEnvironment;

  @IsString()
  globalApiPrefix: string;
}

export function envValidation() {
const config = getEnvConfig();
  const validatedConfig = plainToClass(Environment, config, { enableImplicitConversion: true });
  const errors = validateSync(validatedConfig, { skipMissingProperties: false });

  if (errors.length > 0) {
    throw new Error(errors.toString());
  }
  return validatedConfig;
}
Войдите в полноэкранный режим Выйдите из полноэкранного режима

В качестве краткого пояснения, мы получаем переменные окружения с помощью функции, созданной в начале, затем с помощью функции-трансформера создаем объекты выше, которые содержат необходимые валидаторы. Затем мы проверяем каждое свойство. Если валидация не прошла, приложение не будет запущено.

Теперь нам нужно только запустить валидацию.
Давайте перейдем в app.module.ts и изменим

ConfigModule.forRoot({
      load: [getEnvConfig],
      isGlobal: true,
      cache: true,
    }),
Войти в полноэкранный режим Выйти из полноэкранного режима

на

  ConfigModule.forRoot({
      load: [getEnvConfig],
      isGlobal: true,
      cache: true,
      validate: envValidation,
    })
Войти в полноэкранный режим Выйти из полноэкранного режима

Использование

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

Давайте проверим один из вариантов использования. В частности, порт.
Мы можем обновить main.ts, чтобы использовать ConfigService и получить переменную env.
Таким образом, из чего-то вроде

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.setGlobalPrefix('api');
  await app.listen(3000);
}
Войти в полноэкранный режим Выйти из полноэкранного режима

мы можем получить

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const config: ConfigService = app.get(ConfigService);
  const globalApiPrefix = config.get<IEnvironment['globalApiPrefix']>('globalPrefix') ?? 'api';

  app.setGlobalPrefix(globalApiPrefix);

  const port = config.get<IEnvironment['port']>('port') ?? 3333;
  await app.listen(port);
}
Ввести полноэкранный режим Выйти из полноэкранного режима

Спасибо за прочтение!

Оцените статью
Procodings.ru
Добавить комментарий