Платформа Node.js як середовище виконання

Змінні оточення та конфігурація

process.env, файли .env, dotenv, валідація конфігурації, безпечне управління секретами

Змінні оточення та конфігурація

🎯 Мета лекції

  • Зрозуміти роль змінних оточення (environment variables) у конфігурації застосунків.
  • Опанувати доступ до змінних через process.env та механізми передачі значень.
  • Навчитися використовувати файли .env та бібліотеку dotenv для локальної розробки.
  • Освоїти паттерни валідації та типізації конфігурації у TypeScript.
  • Вивчити best practices для безпечного зберігання секретів та ключів API.

🔑 Ключові терміни

  • Environment Variables: змінні операційної системи, доступні процесу під час виконання.
  • process.env: глобальний об'єкт Node.js, що містить усі змінні оточення у вигляді рядків.
  • dotenv: бібліотека для завантаження змінних із файлу .env у process.env.
  • NODE_ENV: конвенційна змінна для визначення середовища виконання (development, production, test).
  • Secrets Management: практики безпечного зберігання конфіденційних даних (паролів, API ключів, токенів).

Проблема конфігурації: від хардкоду до змінних оточення

Антипаттерн: хардкодинг конфігурації

Найпростіший, але найнебезпечніший спосіб конфігурації — зберігання значень безпосередньо у коді:

// ❌ НЕБЕЗПЕЧНО: Секрети у вихідному коді
const config = {
  database: {
    host: 'prod-db-cluster.amazonaws.com',
    port: 5432,
    username: 'admin',
    password: 'SuperSecret123!', // Витік у git історію!
  },
  apiKeys: {
    stripe: 'sk_live_51H8xYz...', // Публічний доступ через GitHub!
    sendgrid: 'SG.abc123...',
  },
  server: {
    port: 3000,
  },
};

export default config;

Проблеми цього підходу:

  1. Витік секретів: Паролі та API ключі потрапляють у систему контролю версій (Git) і стають доступними всім, хто має доступ до репозиторію.
  2. Відсутність гнучкості: Для зміни конфігурації між середовищами (локальна розробка, staging, production) потрібно редагувати код або підтримувати кілька файлів конфігурації.
  3. Порушення принципу Twelve-Factor App: Сучасні хмарні платформи (Heroku, AWS, Vercel) передають конфігурацію через змінні оточення, а не файли.
Ніколи не комітьте у Git файли з секретами або приватними ключами. Навіть після видалення вони залишаються у історії репозиторію та можуть бути витягнуті зловмисниками через git log або GitHub commit history.

Рішення: змінні оточення як контракт конфігурації

Змінні оточення (environment variables) — це пари ключ-значення, які операційна система надає процесу під час його запуску. Вони існують поза кодом застосунку і встановлюються адміністратором системи, CI/CD-пайплайном або платформою хостингу.

Переваги:

  • Розділення коду та конфігурації: Один і той самий код може працювати у різних середовищах без змін.
  • Безпека: Секрети зберігаються у зашифрованих сховищах (AWS Secrets Manager, HashiCorp Vault) або передаються через захищені канали.
  • Простота розгортання: Хмарні платформи автоматично інжектують змінні у контейнери та процеси.
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF

package "Операційна система" as OS #F1F5F9 {
  rectangle "Змінні оточення\nDB_HOST=localhost\nDB_PORT=5432\nAPI_KEY=secret" as EnvVars #DBEAFE
}

package "Node.js Process" as NodeProc #DCFCE7 {
  rectangle "process.env" as ProcessEnv #E2E8F0
  rectangle "Застосунок" as App #FEF3C7
}

package "Джерела конфігурації" as Sources #F1F5F9 {
  file ".env файл" as DotEnv #DBEAFE
  cloud "Secrets Manager\n(AWS/Vault)" as SecretsManager #FEF3C7
  rectangle "CI/CD Pipeline\n(GitHub Actions)" as CICD #E2E8F0
}

DotEnv --> EnvVars : dotenv.config()
SecretsManager --> EnvVars : Інжектування
CICD --> EnvVars : Встановлення

EnvVars --> ProcessEnv : Передача при старті
ProcessEnv --> App : Читання через\nprocess.env.DB_HOST

@enduml

Доступ до змінних оточення: process.env

Глобальний об'єкт process.env є словником (dictionary) усіх змінних оточення, доступних поточному процесу Node.js. Всі значення зберігаються як рядки (string).

Базове читання змінних

// Читання змінної NODE_ENV
const environment = process.env.NODE_ENV;
console.log('Середовище:', environment); // "development" | "production" | undefined

// Читання порту сервера з fallback
const port = process.env.PORT || '3000';
console.log('Порт:', port); // "8080" або "3000"

// Перевірка наявності критичної змінної
if (!process.env.DATABASE_URL) {
  throw new Error('DATABASE_URL не встановлено! Неможливо підключитися до БД.');
}

const dbUrl = process.env.DATABASE_URL;
console.log('База даних:', dbUrl);
Всі значення у process.env є рядками! Навіть якщо ви встановите PORT=3000, process.env.PORT буде "3000" (string), а не 3000 (number). Для числових значень потрібне явне перетворення через parseInt() або Number().

Типові помилки при роботі з process.env

// ❌ Неправильно: очікування числа
const maxConnections = process.env.MAX_CONNECTIONS || 10;
console.log(maxConnections); // "50" (рядок!) або 10

// ✅ Правильно: явне перетворення типів
const maxConnections = parseInt(process.env.MAX_CONNECTIONS || '10', 10);
console.log(maxConnections); // 50 (число)

// ❌ Неправильно: булеві значення
const isProduction = process.env.NODE_ENV === 'production' || false;
console.log(isProduction); // Завжди true, якщо NODE_ENV !== 'production'!

// ✅ Правильно: явна перевірка
const isProduction = process.env.NODE_ENV === 'production';
console.log(isProduction); // true або false

// ❌ Неправильно: парсинг JSON без обробки помилок
const allowedOrigins = JSON.parse(process.env.CORS_ORIGINS);

// ✅ Правильно: безпечний парсинг
const allowedOrigins = process.env.CORS_ORIGINS
  ? JSON.parse(process.env.CORS_ORIGINS)
  : ['http://localhost:3000'];

Встановлення змінних у різних операційних системах

Unix/Linux/macOS: експорт через shell

У Unix-подібних системах змінні встановлюються через команду export або inline перед командою запуску:

# Експорт змінної для всіх наступних команд у сесії
export NODE_ENV=production
export PORT=8080
export DATABASE_URL="postgresql://user:pass@localhost/mydb"

# Запуск застосунку (змінні вже у process.env)
node server.js

# Або inline (змінні діють лише для цієї команди)
NODE_ENV=production PORT=8080 node server.js
Inline-синтаксис (VAR=value command) працює лише у Unix shell (Bash, Zsh, Fish). У Windows потрібно використовувати set або $env: перед командою, або застосувати кросплатформний пакет cross-env.

Кросплатформний запуск: cross-env

Для універсальних npm-скриптів, які працюють на всіх ОС, використовують пакет cross-env:

npm install --save-dev cross-env

Файли .env: локальна розробка без хаосу

Для локальної розробки зручно зберігати змінні у текстовому файлі .env у корені проєкту. Цей файл не комітиться у Git і містить секрети, специфічні для локального середовища розробника.

Структура файлу .env

# .env — приклад файлу для локальної розробки

# Середовище виконання
NODE_ENV=development

# Сервер
PORT=3000
HOST=localhost

# База даних PostgreSQL
DATABASE_URL=postgresql://postgres:password@localhost:5432/myapp_dev
DB_POOL_MIN=2
DB_POOL_MAX=10

# Redis
REDIS_URL=redis://localhost:6379

# API ключі (секрети)
STRIPE_SECRET_KEY=sk_test_51Hxyz...
SENDGRID_API_KEY=SG.abc123...
JWT_SECRET=my-super-secret-jwt-key-change-in-production

# Зовнішні сервіси
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
AWS_REGION=eu-central-1

# Feature flags
ENABLE_BETA_FEATURES=true
LOG_LEVEL=debug
Файл .env має бути обов'язково доданий у .gitignore! Інакше всі секрети потраплять у публічний репозиторій.

Додавання .env у .gitignore

# .gitignore

# Змінні оточення з секретами
.env
.env.local
.env.*.local

# Дозволити .env.example (шаблон без секретів)
!.env.example

Створення .env.example як шаблону

Файл .env.example комітиться у Git і слугує документацією для інших розробників. Він містить всі необхідні змінні, але без реальних значень:

# .env.example — шаблон конфігурації для розробників

NODE_ENV=development
PORT=3000

# База даних PostgreSQL (встановіть свої значення)
DATABASE_URL=postgresql://USER:PASSWORD@localhost:5432/DATABASE_NAME
DB_POOL_MIN=2
DB_POOL_MAX=10

# Redis
REDIS_URL=redis://localhost:6379

# API ключі (отримайте на https://dashboard.stripe.com)
STRIPE_SECRET_KEY=sk_test_...
SENDGRID_API_KEY=SG...
JWT_SECRET=generate-random-secret-here

# AWS (для локальної розробки використовуйте LocalStack або MinIO)
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_REGION=eu-central-1

Новий розробник у команді копіює .env.example → .env і заповнює власними значеннями:

cp .env.example .env
# Редагувати .env та додати справжні секрети

Завантаження .env файлів: бібліотека dotenv

Пакет dotenv читає файл .env та автоматично додає всі змінні у process.env при старті застосунку. Це стандарт де-факто у Node.js-екосистемі.

Встановлення та базове використання

npm install dotenv

Завантаження змінних на старті застосунку:

// src/server.ts — точка входу
import 'dotenv/config'; // Автоматично завантажує .env у process.env

import express from 'express';

const app = express();
const port = process.env.PORT || 3000;

app.get('/', (req, res) => {
  res.json({
    environment: process.env.NODE_ENV,
    database: process.env.DATABASE_URL?.split('@')[1], // Приховати credentials
  });
});

app.listen(port, () => {
  console.log(`Сервер запущено на http://localhost:${port}`);
  console.log(`Середовище: ${process.env.NODE_ENV}`);
});
Імпорт dotenv/config має бути першим у точці входу застосунку, до імпорту будь-яких модулів, які залежать від process.env. Інакше змінні не встигнуть завантажитися.

Альтернативний синтаксис з явною конфігурацією

import dotenv from 'dotenv';
import path from 'node:path';

// Завантаження .env з кастомного шляху
dotenv.config({
  path: path.resolve(process.cwd(), '.env.local'),
});

// Або із обробкою помилок
const result = dotenv.config();

if (result.error) {
  console.warn('⚠️  Файл .env не знайдено, використовуються системні змінні');
} else {
  console.log('✅ Завантажено змінних:', Object.keys(result.parsed || {}).length);
}

Багатосередовищна конфігурація: .env.development, .env.production

Для різних середовищ можна використовувати окремі файли:

project/
├── .env                  # Базові змінні (загальні для всіх)
├── .env.development      # Локальна розробка
├── .env.test             # Тестове середовище (Jest/Vitest)
├── .env.production       # Production (не комітиться!)
└── .env.example          # Шаблон для розробників

Завантаження правильного файлу залежно від NODE_ENV:

import dotenv from 'dotenv';
import path from 'node:path';

const env = process.env.NODE_ENV || 'development';
const envFile = `.env.${env}`;

// Завантажити базовий .env
dotenv.config();

// Перезаписати специфічними для середовища змінними
dotenv.config({
  path: path.resolve(process.cwd(), envFile),
  override: true, // Перезаписати існуючі змінні
});

console.log(`Завантажено конфігурацію для середовища: ${env}`);
Пакет dotenv-flow автоматизує завантаження багатьох файлів .env у правильному порядку пріоритетів. Він завантажує .env → .env.local → .env.${NODE_ENV} → .env.${NODE_ENV}.local, перезаписуючи попередні значення.

NODE_ENV: конвенція середовища виконання

Змінна NODE_ENV є неофіційним стандартом у Node.js-екосистемі для визначення режиму роботи застосунку. Більшість фреймворків та бібліотек (Express, React, Webpack) використовують її для увімкнення/вимкнення оптимізацій.

Стандартні значення NODE_ENV

development

Локальна розробка

  • Детальні логи та stack traces
  • Гаряча перезагрузка (hot reload)
  • Вимкнені оптимізації (швидша збірка)
  • Source maps для налагодження

production

Продакшн-сервери

  • Мінімальні логи (тільки помилки)
  • Мініфікація та стиснення коду
  • Увімкнені оптимізації (кешування, gzip)
  • Відключені dev tools

test

Автоматичне тестування

  • Швидка база даних у пам'яті
  • Моки зовнішніх API
  • Паралельне виконання тестів
  • Очищення стану між тестами

Використання NODE_ENV у коді

const isDevelopment = process.env.NODE_ENV === 'development';
const isProduction = process.env.NODE_ENV === 'production';
const isTest = process.env.NODE_ENV === 'test';

// Умовне логування
if (isDevelopment) {
  console.log('[DEBUG] Деталі запиту:', req.body);
}

// Різна конфігурація для різних середовищ
const logLevel = isProduction ? 'error' : 'debug';

// Увімкнення CORS тільки у розробці
if (isDevelopment) {
  app.use(cors({ origin: '*' }));
}

// Різні бази даних
const databaseUrl = isTest
  ? 'postgresql://localhost/test_db'
  : process.env.DATABASE_URL;

Express: автоматична оптимізація за NODE_ENV

Express автоматично змінює поведінку залежно від NODE_ENV:

import express from 'express';

const app = express();

// У production Express:
// - Кешує шаблони view
// - Вимикає детальні помилки у відповідях
// - Оптимізує роутинг

if (process.env.NODE_ENV === 'production') {
  app.enable('view cache');
  app.set('trust proxy', 1); // За reverse proxy (Nginx, CloudFlare)
}

// Middleware для обробки помилок залежно від середовища
app.use((err, req, res, next) => {
  const isDev = process.env.NODE_ENV !== 'production';

  res.status(err.status || 500).json({
    error: {
      message: err.message,
      // Stack trace тільки у розробці
      ...(isDev && { stack: err.stack }),
    },
  });
});

Типізація та валідація конфігурації у TypeScript

Прямий доступ до process.env не є type-safe: TypeScript не може гарантувати наявність змінних або їхній формат. Для надійності потрібна централізована валідація конфігурації.

Проблема: відсутність типів у process.env

// ❌ Небезпечно: TypeScript не перевіряє наявність змінної
const apiKey = process.env.STRIPE_API_KEY; // string | undefined
// Застосунок може впасти у runtime, якщо змінна відсутня

// ❌ Небезпечно: неявне перетворення типів
const port = process.env.PORT; // string | undefined
app.listen(port); // Помилка: очікується number, отримано string

Рішення 1: Ручна валідація з типами

// src/config/env.ts
interface EnvironmentConfig {
  nodeEnv: 'development' | 'production' | 'test';
  port: number;
  database: {
    url: string;
    poolMin: number;
    poolMax: number;
  };
  apiKeys: {
    stripe: string;
    sendgrid: string;
  };
  jwt: {
    secret: string;
    expiresIn: string;
  };
}

function validateEnv(): EnvironmentConfig {
  const requiredVars = [
    'NODE_ENV',
    'PORT',
    'DATABASE_URL',
    'STRIPE_SECRET_KEY',
    'SENDGRID_API_KEY',
    'JWT_SECRET',
  ];

  // Перевірка наявності критичних змінних
  const missing = requiredVars.filter((varName) => !process.env[varName]);

  if (missing.length > 0) {
    throw new Error(
      `❌ Відсутні обов'язкові змінні оточення: ${missing.join(', ')}`
    );
  }

  // Валідація NODE_ENV
  const nodeEnv = process.env.NODE_ENV as string;
  if (!['development', 'production', 'test'].includes(nodeEnv)) {
    throw new Error(
      `❌ NODE_ENV має бути development, production або test. Отримано: ${nodeEnv}`
    );
  }

  // Валідація PORT
  const port = parseInt(process.env.PORT as string, 10);
  if (isNaN(port) || port < 1 || port > 65535) {
    throw new Error(`❌ PORT має бути числом від 1 до 65535. Отримано: ${process.env.PORT}`);
  }

  return {
    nodeEnv: nodeEnv as EnvironmentConfig['nodeEnv'],
    port,
    database: {
      url: process.env.DATABASE_URL as string,
      poolMin: parseInt(process.env.DB_POOL_MIN || '2', 10),
      poolMax: parseInt(process.env.DB_POOL_MAX || '10', 10),
    },
    apiKeys: {
      stripe: process.env.STRIPE_SECRET_KEY as string,
      sendgrid: process.env.SENDGRID_API_KEY as string,
    },
    jwt: {
      secret: process.env.JWT_SECRET as string,
      expiresIn: process.env.JWT_EXPIRES_IN || '7d',
    },
  };
}

// Єдине джерело істини для конфігурації
export const config = validateEnv();

Використання у застосунку:

// src/server.ts
import 'dotenv/config';
import { config } from './config/env';
import express from 'express';

const app = express();

// Type-safe доступ до конфігурації
app.listen(config.port, () => {
  console.log(`🚀 Сервер запущено на порту ${config.port}`);
  console.log(`📦 Середовище: ${config.nodeEnv}`);
  console.log(`🗄️  База даних: ${config.database.url.split('@')[1]}`);
});

// TypeScript знає, що config.port — це number
const maxTimeout = config.port * 10; // OK
Винесення валідації конфігурації у окремий модуль забезпечує fail-fast поведінку: застосунок відмовляється запускатися з некоректною конфігурацією, замість того щоб падати під час обробки запитів.

Рішення 2: Валідація через Zod

Бібліотека zod надає декларативний синтаксис для валідації та автоматичної генерації TypeScript типів:

npm install zod
npm run dev (з відсутніми змінними)
$ npm run dev
❌ Помилка валідації змінних оточення:
{
DATABASE_URL: {
_errors: [ 'Required' ]
},
STRIPE_SECRET_KEY: {
_errors: [ 'String must start with "sk_"' ]
},
JWT_SECRET: {
_errors: [ 'JWT_SECRET має містити мінімум 32 символи' ]
}
}
Використання zod для валідації змінних оточення є best practice у сучасних TypeScript-проєктах. Бібліотека автоматично генерує типи, забезпечує детальні повідомлення про помилки та підтримує трансформації (string → number, string → boolean).

Best Practices: безпека та управління секретами

1. Ніколи не комітьте секрети у Git

# .gitignore — обов'язкові правила

# Файли з секретами
.env
.env.local
.env.*.local
*.pem
*.key
*.crt

# Виняток: шаблони без секретів
!.env.example

Перевірка перед комітом:

# Пошук потенційних секретів у staged файлах
git diff --cached --name-only | xargs grep -E 'password|secret|api_key' || echo "✅ Секретів не знайдено"

2. Використовуйте різні секрети для різних середовищ

// ❌ Небезпечно: один ключ для всіх середовищ
const stripeKey = 'sk_live_abc123'; // Якщо витече — зламано production!

// ✅ Правильно: різні ключі для dev та prod
const stripeKey =
  process.env.NODE_ENV === 'production'
    ? process.env.STRIPE_LIVE_SECRET_KEY
    : process.env.STRIPE_TEST_SECRET_KEY;

3. Шифруйте .env у приватних репозиторіях (опціонально)

Для команд, які зберігають .env у Git (не рекомендується для публічних репозиторіїв), можна використовувати git-crypt або sops:

# Встановлення git-crypt (macOS)
brew install git-crypt

# Ініціалізація шифрування у репозиторії
git-crypt init

# Додати користувача з GPG ключем
git-crypt add-gpg-user USER_GPG_KEY

# Зазначити файли для шифрування
echo ".env filter=git-crypt diff=git-crypt" >> .gitattributes

# Після комміту .env буде автоматично зашифрований
git add .env .gitattributes
git commit -m "Add encrypted .env"

4. Генеруйте криптографічно стійкі секрети

import crypto from 'node:crypto';

// Генерація випадкового JWT secret (32 байти = 256 біт)
const jwtSecret = crypto.randomBytes(32).toString('hex');
console.log('JWT_SECRET=' + jwtSecret);

// Генерація безпечного паролю (base64 для читабельності)
const password = crypto.randomBytes(24).toString('base64');
console.log('DATABASE_PASSWORD=' + password);
node generate-secrets.ts
$ node generate-secrets.ts
JWT_SECRET=a7f3c8e9d2b1f4a6c8e0d2f5b7a9c1e3f5b8d0a2c4e6f8a0c2e4f6a8b0c2d4e6
DATABASE_PASSWORD=kJ8mNq3pRt6vYz9wXc2bAf5eHg7jLo1iSu4d

5. Використовуйте Secrets Manager у production

Для production-середовищ використовуйте спеціалізовані сервіси управління секретами:

AWS Secrets Manager

Централізоване сховище секретів з автоматичною ротацією та аудитом доступу. Інтеграція через AWS SDK.

HashiCorp Vault

Самохостинг рішення з підтримкою динамічних секретів, шифрування та детального контролю доступу (ACL).

Doppler

Хмарна платформа для синхронізації змінних оточення між командою та середовищами. CLI для локальної розробки.

Приклад інтеграції з AWS Secrets Manager:

import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';

const client = new SecretsManagerClient({ region: process.env.AWS_REGION });

async function getSecret(secretName: string): Promise<string> {
  const command = new GetSecretValueCommand({ SecretId: secretName });
  const response = await client.send(command);

  if (!response.SecretString) {
    throw new Error(`Секрет ${secretName} не містить значення`);
  }

  return response.SecretString;
}

// Завантаження секретів при старті
const dbPassword = await getSecret('prod/database/password');
const stripeKey = await getSecret('prod/stripe/secret-key');

console.log('✅ Секрети завантажено з AWS Secrets Manager');

6. Логуйте конфігурацію без секретів

function logConfig(config: Record<string, any>, secretKeys: string[] = []): void {
  const sanitized = { ...config };

  // Приховати значення секретних ключів
  secretKeys.forEach((key) => {
    if (sanitized[key]) {
      sanitized[key] = '***HIDDEN***';
    }
  });

  console.log('📋 Конфігурація застосунку:', JSON.stringify(sanitized, null, 2));
}

// Використання
logConfig(
  {
    NODE_ENV: process.env.NODE_ENV,
    PORT: process.env.PORT,
    DATABASE_URL: process.env.DATABASE_URL,
    JWT_SECRET: process.env.JWT_SECRET,
  },
  ['DATABASE_URL', 'JWT_SECRET'] // Не показувати у логах
);
node server.ts
$ node server.ts
📋 Конфігурація застосунку:
{
"NODE_ENV": "production",
"PORT": "8080",
"DATABASE_URL": "***HIDDEN***",
"JWT_SECRET": "***HIDDEN***"
}

Практичні приклади конфігурації

Приклад 1: Express сервер з базою даних

// src/config/env.ts
import { z } from 'zod';

const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
  PORT: z.string().transform(Number).pipe(z.number().positive()),
  HOST: z.string().default('0.0.0.0'),

  DATABASE_URL: z.string().url(),
  DB_SSL: z.string().transform((v) => v === 'true').default('false'),

  REDIS_URL: z.string().url().optional(),

  CORS_ORIGINS: z
    .string()
    .transform((val) => val.split(',').map((s) => s.trim()))
    .pipe(z.array(z.string().url())),

  RATE_LIMIT_MAX: z.string().transform(Number).pipe(z.number().positive()).default('100'),
  RATE_LIMIT_WINDOW_MS: z.string().transform(Number).default('900000'), // 15 хвилин
});

export const env = envSchema.parse(process.env);
// src/server.ts
import 'dotenv/config';
import express from 'express';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
import { Pool } from 'pg';
import { createClient } from 'redis';
import { env } from './config/env';

// База даних PostgreSQL
const pool = new Pool({
  connectionString: env.DATABASE_URL,
  ssl: env.DB_SSL ? { rejectUnauthorized: false } : false,
});

// Redis для сесій
const redis = env.REDIS_URL ? await createClient({ url: env.REDIS_URL }).connect() : null;

// Express застосунок
const app = express();

// CORS з дозволеними origins
app.use(cors({ origin: env.CORS_ORIGINS, credentials: true }));

// Rate limiting
app.use(
  rateLimit({
    windowMs: env.RATE_LIMIT_WINDOW_MS,
    max: env.RATE_LIMIT_MAX,
    message: 'Забагато запитів з цієї IP-адреси, спробуйте пізніше.',
  })
);

app.get('/health', async (req, res) => {
  const dbStatus = await pool.query('SELECT 1').then(() => 'OK').catch(() => 'ERROR');
  const redisStatus = redis ? await redis.ping().then(() => 'OK').catch(() => 'ERROR') : 'N/A';

  res.json({
    status: 'UP',
    environment: env.NODE_ENV,
    database: dbStatus,
    redis: redisStatus,
  });
});

app.listen(env.PORT, env.HOST, () => {
  console.log(`🚀 Сервер запущено на http://${env.HOST}:${env.PORT}`);
  console.log(`📦 Середовище: ${env.NODE_ENV}`);
  console.log(`🔒 CORS origins: ${env.CORS_ORIGINS.join(', ')}`);
});

Приклад 2: Багатосередовищна конфігурація з fallback

// src/config/database.ts
import { env } from './env';

interface DatabaseConfig {
  host: string;
  port: number;
  database: string;
  username: string;
  password: string;
  ssl: boolean;
  pool: {
    min: number;
    max: number;
  };
}

export function getDatabaseConfig(): DatabaseConfig {
  // Парсинг DATABASE_URL: postgresql://user:pass@host:port/dbname
  const url = new URL(env.DATABASE_URL);

  return {
    host: url.hostname,
    port: parseInt(url.port || '5432', 10),
    database: url.pathname.slice(1), // Видалити початковий /
    username: url.username,
    password: url.password,
    ssl: env.NODE_ENV === 'production',
    pool: {
      min: env.NODE_ENV === 'production' ? 5 : 2,
      max: env.NODE_ENV === 'production' ? 20 : 10,
    },
  };
}

Питання для самоконтролю

Підсумок: правила роботи зі змінними оточення

✅ Робити

  • Використовувати .env для локальної розробки
  • Додавати .env у .gitignore
  • Створювати .env.example як документацію
  • Валідувати конфігурацію при старті (Zod)
  • Генерувати криптостійкі секрети
  • Використовувати різні ключі для dev/prod
  • Логувати конфігурацію без секретів
  • Встановлювати NODE_ENV явно

❌ Не робити

  • Комітити .env у Git
  • Хардкодити секрети у коді
  • Використовувати production-ключі локально
  • Ігнорувати валідацію змінних
  • Логувати паролі та API ключі
  • Змінювати process.env у runtime
  • Зберігати секрети у CI/CD логах
  • Використовувати один .env для всіх середовищ
Золоте правило: Конфігурація має бути зовнішньою до коду. Застосунок не повинен знати, де він запущений — локально, у staging чи production. Середовище повідомляє застосунку його конфігурацію через змінні оточення.

Чеклист для production

Крок 1: Валідація конфігурації

Переконайтеся, що всі критичні змінні присутні та мають правильний формат через Zod або власну функцію валідації.

Крок 2: Secrets Manager

Перенесіть секрети з .env у AWS Secrets Manager, HashiCorp Vault або аналогічний сервіс.

Крок 3: Різні ключі для середовищ

Переконайтеся, що production використовує sk_live_* (Stripe), prod-* (AWS) ключі, а не тестові.

Крок 4: Налаштування NODE_ENV

Встановіть NODE_ENV=production у Dockerfile, docker-compose.yml або платформі хостингу (Vercel, Railway).

Крок 5: Логування без секретів

Перевірте, що логи не містять паролів, API ключів або токенів. Використовуйте sanitization функції.

Крок 6: Моніторинг доступу

Увімкніть аудит логи у Secrets Manager для відстеження, хто та коли отримував доступ до секретів.

Діаграма: життєвий цикл конфігурації

Loading diagram...
graph TD
    A[Розробник створює .env.example] -->|Комітить у Git| B[Репозиторій GitHub]
    B -->|Новий розробник клонує| C[Локальна машина]
    C -->|cp .env.example .env| D[Заповнює локальні секрети]
    D -->|dotenv.config| E[process.env]
    
    B -->|CI/CD Pipeline| F[GitHub Actions]
    F -->|Secrets з GitHub Settings| G[Encrypted Secrets]
    G -->|Інжектування при deploy| H[Production Server]
    
    H -->|AWS SDK| I[AWS Secrets Manager]
    I -->|getSecret| J[Runtime Configuration]
    
    E -->|Локальна розробка| K[npm run dev]
    J -->|Production| L[node dist/server.js]
    
    style A fill:#DBEAFE
    style D fill:#FEF3C7
    style E fill:#DCFCE7
    style G fill:#FEE2E2
    style I fill:#FEE2E2
    style J fill:#DCFCE7

У наступній лекції ми розглянемо інструменти налагодження Node.js-застосунків, включаючи вбудований debugger, Chrome DevTools та профілювання продуктивності.

Copyright © 2026