Пошук уроків, статей та іншого контенту
Налаштуєте змінні середовища, конфігураційні файли та безпечне керування параметрами застосунку.
У застосунку зазвичай є параметри, які відрізняються між середовищами:
порт HTTP-сервера;
режим роботи застосунку;
адреса бази даних;
ключі для підпису токенів;
облікові дані зовнішніх сервісів.
Такі значення не варто вбудовувати безпосередньо в код. Для цього використовують змінні середовища та пакет @nestjs/config.
Переваги такого підходу:
один і той самий код працює локально, у тестах і production;
секрети не зберігаються у вихідному коді;
неправильна конфігурація виявляється під час запуску;
конфігурацію можна централізовано читати через ConfigService.
@nestjs/configВстановіть пакет конфігурації:
npm install @nestjs/configДля валідації змінних середовища встановіть також joi:
npm install joi.envСтворіть у корені проєкту файл .env:
NODE_ENV=development
PORT=3000
DB_HOST=localhost
DB_PORT=5432
DB_NAME=app
DB_USER=app_user
DB_PASSWORD=local_passwordЗначення з .env автоматично завантажуються в process.env, якщо підключити ConfigModule.
Файл .env не слід додавати до репозиторію, особливо якщо він містить паролі або ключі.
Додайте його до .gitignore:
.env
.env.local
.env.productionДля команди можна створити безпечний шаблон .env.example без справжніх секретів:
NODE_ENV=development
PORT=3000
DB_HOST=localhost
DB_PORT=5432
DB_NAME=app
DB_USER=app_user
DB_PASSWORD=change_meФайл .env.example можна зберігати в репозиторії. Він показує, які змінні потрібні застосунку.
ConfigModuleУ app.module.ts підключіть глобальний модуль конфігурації:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.local', '.env'],
validationSchema: Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'test', 'production')
.default('development'),
PORT: Joi.number()
.port()
.default(3000),
DB_HOST: Joi.string()
.required(),
DB_PORT: Joi.number()
.port()
.default(5432),
DB_NAME: Joi.string()
.required(),
DB_USER: Joi.string()
.required(),
DB_PASSWORD: Joi.string()
.required(),
}),
}),
],
})
export class AppModule {}Основні параметри:
isGlobal: true робить ConfigService доступним в інших модулях без повторного імпорту ConfigModule;
envFilePath задає файли, з яких завантажуються змінні;
validationSchema перевіряє конфігурацію під час запуску;
default задає значення за замовчуванням;
required вимагає наявність змінної.
У цьому прикладі .env.local має вищий пріоритет, ніж .env. Якщо однакова змінна є в обох файлах, використовується значення з .env.local.
ConfigServiceНеобхідні значення отримують через ConfigService.
Наприклад, створимо сервіс, який читає порт і параметри бази даних:
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AppService {
constructor(private readonly configService: ConfigService) {}
getConfiguration() {
return {
environment: this.configService.get<string>('NODE_ENV'),
port: this.configService.get<number>('PORT'),
database: {
host: this.configService.get<string>('DB_HOST'),
port: this.configService.get<number>('DB_PORT'),
name: this.configService.get<string>('DB_NAME'),
},
};
}
}ConfigService приймає назву змінної та повертає її значення.
Паролі, токени та інші секрети не слід повертати з контролерів або записувати в логи.
Щоб приклад можна було перевірити, підключимо сервіс до контролера:
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('configuration')
getConfiguration() {
return this.appService.getConfiguration();
}
}Модуль застосунку:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.local', '.env'],
validationSchema: Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'test', 'production')
.default('development'),
PORT: Joi.number().port().default(3000),
DB_HOST: Joi.string().required(),
DB_PORT: Joi.number().port().default(5432),
DB_NAME: Joi.string().required(),
DB_USER: Joi.string().required(),
DB_PASSWORD: Joi.string().required(),
}),
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}Запустіть застосунок:
npm run start:devПісля цього запит до GET /configuration поверне нешкідливі параметри конфігурації:
{
"environment": "development",
"port": 3000,
"database": {
"host": "localhost",
"port": 5432,
"name": "app"
}
}У реальному production-застосунку подібний службовий endpoint зазвичай не створюють або захищають доступ до нього.
Коли змінних стає багато, зручно групувати їх у конфігураційних файлах. Наприклад, створіть src/config/database.config.ts:
import { registerAs } from '@nestjs/config';
export default registerAs('database', () => ({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
name: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
}));Функція registerAs створює конфігурацію з простором імен database.
Підключіть її в app.module.ts через параметр load:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
import databaseConfig from './config/database.config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [databaseConfig],
validationSchema: Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'test', 'production')
.default('development'),
PORT: Joi.number().port().default(3000),
DB_HOST: Joi.string().required(),
DB_PORT: Joi.number().port().default(5432),
DB_NAME: Joi.string().required(),
DB_USER: Joi.string().required(),
DB_PASSWORD: Joi.string().required(),
}),
}),
],
})
export class AppModule {}Тепер значення можна отримувати за вкладеним ключем:
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class DatabaseSettingsService {
constructor(private readonly configService: ConfigService) {}
getDatabaseSettings() {
return {
host: this.configService.get<string>('database.host'),
port: this.configService.get<number>('database.port'),
name: this.configService.get<string>('database.name'),
user: this.configService.get<string>('database.user'),
};
}
}Такий поділ допомагає не зберігати всі параметри в одному великому файлі модуля:
database.config.ts — конфігурація бази даних;
auth.config.ts — параметри автентифікації;
app.config.ts — загальні параметри застосунку.
Змінні середовища надходять як текст. Наприклад, навіть PORT=3000 спочатку є рядком.
Пакет Joi перевіряє, що значення можна перетворити на число, але під час використання конфігурації бажано явно вказувати тип:
const port = this.configService.get<number>('PORT');Для числових значень у власних конфігураційних файлах використовуйте перетворення:
port: Number(process.env.DB_PORT),Не покладайтеся на неявне перетворення рядків у числових операціях.
У production значення зазвичай передаються платформою розгортання або операційною системою, а не через файл .env, який лежить на сервері в репозиторії.
Наприклад, перед запуском можна встановити змінні середовища:
NODE_ENV=production PORT=8080 DB_HOST=db.internal DB_PORT=5432 DB_NAME=app DB_USER=app_user DB_PASSWORD=strong_password npm run start:prodТипова послідовність запуску NestJS у production:
npm run build
npm run start:prodКоманда npm run build компілює TypeScript у каталог dist, а npm run start:prod запускає зібраний застосунок. Значення конфігурації мають бути доступні процесу в момент запуску.
У production обов’язково потрібно:
передавати всі обов’язкові змінні;
використовувати окремі секрети для production;
не зберігати production .env у Git;
не виводити секрети в консоль;
не задавати паролі за замовчуванням;
перевіряти конфігурацію до запуску сервера.
Якщо обов’язкової змінної немає, застосунок повинен завершити запуск із помилкою, а не працювати з неповною конфігурацією.
Якщо в .env немає, наприклад, DB_PASSWORD, то під час запуску з’явиться помилка валідації. Це очікувана поведінка.
Краще отримати помилку одразу:
Config validation error: "DB_PASSWORD" is requiredніж запустити застосунок, який згодом не зможе підключитися до бази даних або випадково використає неправильні параметри.
Погано:
const jwtSecret = 'my-secret-password';Краще:
const jwtSecret = configService.get<string>('JWT_SECRET');Саму змінну JWT_SECRET потрібно передати через середовище та зробити обов’язковою у схемі валідації.
.env до репозиторіюНавіть якщо файл потрібен локально, він може містити реальні паролі. Перевірте .gitignore і не додавайте .env до комітів.
process.env по всьому застосункуБезпосереднє використання process.env у багатьох файлах ускладнює тестування та контроль конфігурації.
Краще централізовано підключити ConfigModule, перевірити змінні під час запуску та читати їх через ConfigService.
Якщо не перевіряти змінні середовища, помилка може проявитися лише під час виконання конкретної операції. Додавайте validationSchema для обов’язкових і критичних параметрів.
Не повертайте DB_PASSWORD, токени або ключі у відповідях API:
return {
host: configService.get<string>('DB_HOST'),
password: configService.get<string>('DB_PASSWORD'),
};Навіть тимчасовий debug-код може потрапити у production.
ConfigModule з пакета @nestjs/config централізовано керує конфігурацією NestJS.
Значення середовища можна зберігати у .env локально та передавати платформою розгортання у production.
Файл .env із секретами не можна додавати до репозиторію.
ConfigService використовується для читання параметрів у сервісах і контролерах.
Joi допомагає перевірити конфігурацію під час запуску.
registerAs дає змогу групувати параметри в окремих конфігураційних файлах.
Секрети потрібно зберігати поза кодом, не виводити в логи та не повертати через API.