Приветствую, сегодня мы поговорим о переменных окружения в 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);
}
Спасибо за прочтение!