Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як працювати з змінними середовища в Node.js?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Змінні середовища** (environment variables) в Node.js читаються через `process.env` - звичайний JS-об'єкт, заповнений при старті процесу. Всі значення - рядки, тому перетворюй їх перед використанням. ```js require('dotenv').config(); // завантажити .env першим const port = parseInt(process.env.PORT || '3000', 10); const debug = process.env.DEBUG === 'true'; ``` **Ключове:** `dotenv` завантажуй першим рядком вхідного файлу, а `.env` ніколи не пушити до git.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Змінні середовища** (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`. ### Швидкий приклад ```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):** ```bash PORT=4000 node server.js # inline, тільки для цієї команди export PORT=4000 && node server.js # для поточної сесії ``` **Windows:** ```bash # cmd set PORT=4000 && node server.js # PowerShell $env:PORT="4000"; node server.js ``` **npm scripts** (кросплатформенно через пакет `cross-env`): ```json { "scripts": { "start": "cross-env NODE_ENV=production node server.js" } } ``` ### Файли .env з dotenv Для локальної розробки поклади змінні у файл `.env` в кореневій директорії проекту і завантажуй їх через пакет `dotenv`. ```bash npm install dotenv ``` ```ini # .env - ніколи не комітити цей файл PORT=3000 NODE_ENV=development DATABASE_URL=postgresql://user:password@localhost:5432/mydb JWT_SECRET=super-secret-key-32-chars-minimum ``` ```js // Вхідний файл: 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 # специфічні для продакшну ``` ```js dotenv.config({ path: `.env.${process.env.NODE_ENV}` }); ``` ### Вбудована підтримка .env у Node.js 20.6+ Починаючи з Node 20.6, пакет `dotenv` не потрібен для базового використання: ```bash node --env-file=.env server.js ``` Node зчитує файл напряму. Синтаксис такий самий, як і в dotenv. Для більшості проектів цього достатньо. Для кількох файлів або розширення змінних (variable expansion) dotenv дає більше можливостей. ### Валідація змінних при старті Читати `process.env.KEY` і сподіватися, що значення є, - це отримувати крашi під час роботи, а не при старті. Перевіряй один раз і рано: ```js 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`:** ```js // Неправильно app.listen(process.env.PORT); // undefined в локалці → TypeError // Правильно app.listen(parseInt(process.env.PORT || '3000', 10)); ``` **Рядок `'false'` як булевий:** ```js // Неправильно - 'false' є truthy-рядком if (process.env.DEBUG) { enableLogging(); } // Правильно if (process.env.DEBUG === 'true') { enableLogging(); } ``` **Коміт `.env` до git:** ```bash # .gitignore - додай до першого коміту .env .env.local .env.*.local ``` У 2023 році витік, пов'язаний з Vercel, стався через `.env` у публічному репозиторії. Виправлення займає п'ять секунд: додати файл у `.gitignore` до `git init`. **Завантаження dotenv після інших модулів:** ```js // Неправильно - db.js читає DATABASE_URL як undefined const db = require('./db'); require('dotenv').config(); // запізно // Правильно require('dotenv').config(); const db = require('./db'); ``` **Мутація змінних для worker threads:** ```js // Зміни 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 ```js // Запуск: 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 ```js // 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 ```js 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`, знає: валідація вже відбулась і типи відповідають схемі.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.