Як працювати з змінними середовища в Node.js?
Змінні середовища (environment variables) - це пари ключ-значення, які передаються процесу Node.js при старті. Вони дозволяють налаштовувати додатки без жорсткого прописування секретів, портів чи URL баз даних прямо в коді.
Теорія
TL;DR
process.env.KEYзавжди повертає рядок.'false'- це truthy-рядок.'0'теж truthy. Без винятків.dotenvзавантажуй першим рядком, до будь-яких іншихrequire..envдо git не пушити. Додай у.gitignoreдо першого коміту.- Типи перетворюй вручну:
parseInt(...)для чисел,=== 'true'для булевих. - Node 20.6+ вміє читати
.envбез пакетів:node --env-file=.env server.js.
Швидкий приклад
// .env файл:
// PORT=4000
// DEBUG=true
require('dotenv').config(); // обов'язково першим рядком
const port = parseInt(process.env.PORT || '3000', 10);
const debug = process.env.DEBUG === 'true'; // НЕ просто if (process.env.DEBUG)
console.log(port); // 4000
console.log(debug); // true (булевий, не рядок)Тут відбувається два кроки. dotenv зчитує .env і копіює кожну пару в process.env. Далі код читає їх як рядки і перетворює в потрібний тип вручну.
Як Node завантажує змінні середовища
Node.js зчитує змінні від ОС при створенні процесу через C++ біндинги до getenv(). Вони потрапляють в process.env (звичайний JS-об'єкт) ще до того, як завантажується твій головний модуль. Це означає одне: process.env - це знімок (snapshot). Якщо змінити process.env.KEY під час роботи програми, батьківська оболонка нічого не побачить. Якщо інший процес встановить змінну після старту Node, Node теж нічого не дізнається.
Всі значення - рядки. ОС не знає про числа чи булеві типи. Саме тому if (process.env.DEBUG) спрацює навіть коли значення - рядок 'false'.
Встановлення змінних
Три способи залежно від контексту.
Термінал (Linux/macOS):
PORT=4000 node server.js # inline, тільки для цієї команди
export PORT=4000 && node server.js # для поточної сесіїWindows:
# cmd
set PORT=4000 && node server.js
# PowerShell
$env:PORT="4000"; node server.jsnpm scripts (кросплатформенно через пакет cross-env):
{
"scripts": {
"start": "cross-env NODE_ENV=production node server.js"
}
}Файли .env з dotenv
Для локальної розробки поклади змінні у файл .env в кореневій директорії проекту і завантажуй їх через пакет dotenv.
npm install dotenv# .env - ніколи не комітити цей файл
PORT=3000
NODE_ENV=development
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
JWT_SECRET=super-secret-key-32-chars-minimum// Вхідний файл: app.js, index.js або server.js
require('dotenv').config(); // першим рядком, без винятків
const express = require('express');
const app = express();
app.listen(process.env.PORT || 3000);Найпоширеніша помилка, яку я бачу в pull request-ах: require('dotenv').config() стоїть після require('./db'), який вже спробував прочитати DATABASE_URL. Значення було undefined. Виправлення завжди одне - перемістити dotenv на самий початок.
Кілька env-файлів дозволяють розділяти конфігурацію:
.env # спільні значення за замовчуванням
.env.local # локальні налаштування, gitignored
.env.development # специфічні для розробки
.env.production # специфічні для продакшнуdotenv.config({ path: `.env.${process.env.NODE_ENV}` });Вбудована підтримка .env у Node.js 20.6+
Починаючи з Node 20.6, пакет dotenv не потрібен для базового використання:
node --env-file=.env server.jsNode зчитує файл напряму. Синтаксис такий самий, як і в dotenv. Для більшості проектів цього достатньо. Для кількох файлів або розширення змінних (variable expansion) dotenv дає більше можливостей.
Валідація змінних при старті
Читати process.env.KEY і сподіватися, що значення є, - це отримувати крашi під час роботи, а не при старті. Перевіряй один раз і рано:
const { z } = require('zod');
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.string().transform(Number).default('3000'),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
});
const env = envSchema.parse(process.env);
// Якщо DATABASE_URL відсутня - виключення при старті
// А не через 3 години після деплою в продакшнJoi працює так само. Суть: виявляти відсутні змінні в момент запуску програми, а не коли юзер потрапить на роут, що потребує бази даних.
Типові помилки
Немає fallback для process.env.PORT:
// Неправильно
app.listen(process.env.PORT); // undefined в локалці → TypeError
// Правильно
app.listen(parseInt(process.env.PORT || '3000', 10));Рядок 'false' як булевий:
// Неправильно - 'false' є truthy-рядком
if (process.env.DEBUG) { enableLogging(); }
// Правильно
if (process.env.DEBUG === 'true') { enableLogging(); }Коміт .env до git:
# .gitignore - додай до першого коміту
.env
.env.local
.env.*.localУ 2023 році витік, пов'язаний з Vercel, стався через .env у публічному репозиторії. Виправлення займає п'ять секунд: додати файл у .gitignore до git init.
Завантаження dotenv після інших модулів:
// Неправильно - db.js читає DATABASE_URL як undefined
const db = require('./db');
require('dotenv').config(); // запізно
// Правильно
require('dotenv').config();
const db = require('./db');Мутація змінних для worker threads:
// Зміни process.env в main не потрапляють до воркерів
process.env.TEST = 'value';
const { Worker } = require('worker_threads');
new Worker('./worker.js'); // воркер отримує знімок зі старту
// Передавай через workerData
new Worker('./worker.js', { workerData: { test: 'value' } });Де зустрічається
- Express:
app.listen(process.env.PORT || 3000)- стандарт на Heroku і Render - Next.js: префікс
NEXT_PUBLIC_відкриває змінні клієнту, решта залишається на сервері - NestJS:
ConfigModule.forRoot()обгортаєprocess.envтипізованим сервісом - Prisma: читає
DATABASE_URLнапряму для міграцій і запитів - Docker:
ENV PORT=3000в Dockerfile або--env-file .envуdocker run - GitHub Actions: блок
env:у workflow-файлах або repository secrets
Питання для закріплення
Q: Чому всі змінні середовища - рядки?
A: ОС передає їх як рядки. Node копіює їх як є. Немає рантайму, який автоматично перетворює "3000" в 3000.
Q: Що станеться, якщо викликати require('dotenv').config() двічі?
A: За замовчуванням dotenv не перезаписує вже встановлені змінні. Другий виклик нічого не змінює для наявних значень. Змінити це можна через { override: true }.
Q: Як поводяться змінні в cluster mode?
A: cluster.fork() копіює знімок середовища на момент виклику. Якщо оновити process.env після fork-у, воркер цього не побачить. Передавай конфіг через опцію env у cluster.fork({ PORT: '4001' }).
Q: Чи оновлення process.env в main thread потрапляє до worker threads?
A: Ні. Worker threads отримують копію середовища при створенні. Зміни в process.env головного потоку не досягають запущених воркерів. Для цього використовуй workerData або канал повідомлень.
Q: Чи є обмеження на розмір змінних середовища?
A: ОС встановлює ліміт - близько 1MB сумарно на Linux. Великі конфігурації краще зберігати у файлі або менеджері секретів (AWS Secrets Manager, HashiCorp Vault), а не в змінних середовища.
Приклади
Базовий: читання змінної з fallback
// Запуск: PORT=4000 node server.js
// Або просто: node server.js (fallback до 3000)
const port = parseInt(process.env.PORT || '3000', 10);
console.log(`Сервер на порті ${port}`);
// Вивід: Сервер на порті 4000
// Вивід (PORT не задано): Сервер на порті 3000Давай fallback для змінних з розумним значенням за замовчуванням. Виключення при старті роби тільки для тих, що дефолту не мають, наприклад DATABASE_URL.
Проміжний: Express-додаток з dotenv
// npm install express dotenv
require('dotenv').config(); // першим
const express = require('express');
const app = express();
const port = parseInt(process.env.PORT || '3000', 10);
const dbUrl = process.env.DATABASE_URL;
if (!dbUrl) {
console.error('DATABASE_URL обов\'язкова');
process.exit(1);
}
app.get('/health', (req, res) => {
res.json({ status: 'ok', port });
});
app.listen(port, () => console.log(`Слухаємо на порті ${port}`));
// .env:
// PORT=3000
// DATABASE_URL=postgres://user:pass@localhost/mydbЗверни увагу на ранній вихід коли DATABASE_URL відсутня. Краще впасти при старті з чітким повідомленням, ніж падати на першому запиті до бази зі складним стектрейсом.
Розширений: валідація при старті через Zod
require('dotenv').config();
const { z } = require('zod');
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.string().regex(/^\d+$/).transform(Number).default('3000'),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
});
let env;
try {
env = envSchema.parse(process.env);
} catch (err) {
console.error('Невалідні змінні середовища:');
console.error(err.flatten().fieldErrors);
process.exit(1);
}
module.exports = env;
// Імпортуй цей модуль скрізь замість прямого звернення до process.env
// Всі змінні перевірені один раз при стартіЦей патерн дає типізований конфіг по всьому додатку. Будь-який файл, що імпортує env, знає: валідація вже відбулась і типи відповідають схемі.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.