Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Змінні середовища в Next.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Змінні середовища в Next.js** діляться на серверні (без префікса) і публічні (префікс `NEXT_PUBLIC_`) за назвою змінної. Серверні змінні не потрапляють у браузер; публічні вбудовуються в клієнтський бандл під час збірки. ```bash DATABASE_URL=secret # тільки сервер NEXT_PUBLIC_API_URL=https://... # доступно в браузері ``` **Головне:** змінні `NEXT_PUBLIC_` замінюються під час `next build`, а не в рантаймі. Зміни потребують нової збірки.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Змінні середовища в Next.js** - це конфігураційні значення, що завантажуються з `.env` файлів і діляться на серверні та клієнтські за однією простою ознакою: додай `NEXT_PUBLIC_` до назви змінної, щоб вона потрапила в браузер. ## Теорія ### TL;DR - Змінні без префікса доступні тільки на сервері. `DATABASE_URL` поверне `undefined` у браузерному коді. - Додай `NEXT_PUBLIC_` - і змінна потрапить у клієнтський бандл. - Публічні змінні вбудовуються під час `next build`. Щоб зміни застосувались, потрібна нова збірка. - `.env.local` ігнорується git за замовчуванням і має найвищий пріоритет серед усіх `.env` файлів. - Порядок пріоритетів (від вищого до нижчого): `.env.local` > `.env.[mode]` > `.env` ### Швидкий приклад ```bash # .env.local DATABASE_URL=postgresql://localhost:5432/mydb # тільки сервер NEXT_PUBLIC_API_URL=https://api.example.com # доступно в браузері ``` ```tsx // Server Component - бачить усі змінні async function Dashboard() { const db = process.env.DATABASE_URL; // "postgresql://..." const api = process.env.NEXT_PUBLIC_API_URL; // "https://api.example.com" } // Client Component - тільки NEXT_PUBLIC_ 'use client'; function Widget() { const api = process.env.NEXT_PUBLIC_API_URL; // "https://api.example.com" const db = process.env.DATABASE_URL; // undefined } ``` Сервер бачить обидві. Браузер - тільки `NEXT_PUBLIC_` змінні. ### Серверно-клієнтський розподіл Next.js реалізує цей розподіл на рівні бандлера. Змінні без `NEXT_PUBLIC_` завантажуються на сервері через Node.js `dotenv` і ніколи не потрапляють у webpack-бандл. Публічні змінні обробляє webpack `DefinePlugin`, який буквально замінює `process.env.NEXT_PUBLIC_API_URL` на рядок `"https://api.example.com"` у клієнтському JavaScript. Ця заміна відбувається один раз, під час `next build`. Якщо оновити `NEXT_PUBLIC_API_URL` у `.env.production` і запустити `next dev`, браузер все одно отримає старе значення зі збірки. ### Конвенції файлів ``` .env - усі середовища .env.local - локальні переопрацювання (ігнорується git) .env.development - тільки для розробки .env.production - тільки для продакшну .env.test - тільки для тестів ``` `.env.local` перекриває всі інші файли. Використовуй його для значень конкретного розробника і секретів, які не потрібно комітити. ### Що і де зберігати - **URL бази даних, API-ключі, JWT-секрети:** без префікса (тільки сервер) - **Ідентифікатори аналітики, публічні URL, publishable key Stripe:** `NEXT_PUBLIC_` - **Змінні, що змінюються між деплоями на Vercel:** через дашборд Vercel, без префікса - **Значення для локального тестування:** `.env.local` ### Як webpack підставляє публічні змінні Змінні `NEXT_PUBLIC_` статично замінюються у бандлі. Динамічний доступ не працює на клієнті: ```tsx // Працює const url = process.env.NEXT_PUBLIC_API_URL; // НЕ працює в браузері const key = "NEXT_PUBLIC_API_URL"; const url = process.env[key]; // undefined ``` webpack `DefinePlugin` замінює тільки літеральні посилання на `process.env.NEXT_PUBLIC_*`. Доступ через властивість об'єкта повертає `undefined`. ### Типізація через TypeScript Додай файл декларацій для автодоповнення і відловлення друкарських помилок на етапі збірки: ```typescript // env.d.ts declare namespace NodeJS { interface ProcessEnv { DATABASE_URL: string; JWT_SECRET: string; NEXT_PUBLIC_API_URL: string; NODE_ENV: "development" | "production" | "test"; } } ``` Після цього `process.env.DATABASE_URL` матиме тип `string` замість `string | undefined`. Корисно під час рефакторингу. ### Типові помилки **Помилка: читати серверну змінну у `use client` компоненті** ```tsx 'use client'; const secret = process.env.DATABASE_URL; // undefined у браузері ``` Рішення: зчитай змінну у Server Component і передай тільки потрібні дані через пропс. Сам секрет не передавай. **Помилка: закомітити `.env.local` у git** Цей файл ігнорується git не випадково. Перевір `.gitignore`, використовуй дашборд Vercel або GitHub Actions secrets для продакшн-значень і залишай `.env.example` з плейсхолдерами для команди. **Помилка: очікувати, що зміна `NEXT_PUBLIC_` змінної застосується без нової збірки** Публічні змінні вшиваються у бандл під час `next build`. Якщо змінити `.env.production` і запустити `next dev`, браузер покаже старе значення. Потрібна нова збірка. **Помилка: динамічний доступ до публічних змінних** `process.env[variableName]` повертає `undefined` у браузері для `NEXT_PUBLIC_` змінних. Використовуй тільки літеральну форму. ### Де зустрічається у реальних проектах - Stripe: `STRIPE_SECRET_KEY` серверна у Server Components; `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` для клієнтського SDK - Clerk / NextAuth: `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` для ініціалізації браузерного SDK - Supabase: service key серверна у Server Actions; `NEXT_PUBLIC_SUPABASE_URL` для клієнтського SDK - Google Analytics: `NEXT_PUBLIC_GA_ID` у клієнтському компоненті аналітики - PlanetScale / Turso: рядки підключення до БД завжди серверні у API-маршрутах Я не раз бачив, як `NEXT_PUBLIC_` змінні потрапляли в git-історію, бо хтось додав їх у `.env` замість `.env.local`. Зазвичай це publishable key, а не секрет, але звичку перевіряти варто виробити одразу. ### Питання для поглиблення **Q:** Який порядок пріоритетів у `.env` файлів? **A:** `.env.local` має найвищий пріоритет, потім `.env.[mode]` (наприклад `.env.development`), потім `.env`. Перше співпадіння перемагає. **Q:** Чи можна читати `process.env` у `next.config.js`? **A:** Так. `next.config.js` виконується під час збірки на сервері, тому будь-яка змінна доступна там без префікса `NEXT_PUBLIC_`. **Q:** Чому `NEXT_PUBLIC_` змінна повертає `undefined` після нової збірки? **A:** Майже завжди причина в динамічному доступі. `process.env[key]` не працює для публічних змінних. Використовуй пряме посилання: `process.env.NEXT_PUBLIC_YOUR_VAR`. **Q:** (Senior) Чому `process.env` може поводитись інакше у Vercel Edge Runtime? **A:** Edge Runtime не запускає повноцінний Node.js, тому `dotenv` не читає файли з диска. Змінні потрібно задавати в дашборді Vercel, і вони підставляються на рівні платформи, а не з `.env` файлів під час виконання. **Q:** Як перевіряти наявність потрібних змінних при старті? **A:** Використовуй файл `lib/env.ts` із `zod` або простими перевірками: `if (!process.env.DB_URL) throw new Error("Missing DB_URL")`. Впасти при старті набагато легше відлагодити, ніж впасти під час запиту. ## Приклади ### Базовий: серверний vs клієнтський компонент ```tsx // app/page.tsx - Server Component export default async function Home() { const dbUrl = process.env.DATABASE_URL; // Тут повний рядок підключення return <ClientWidget />; } // components/ClientWidget.tsx 'use client'; export function ClientWidget() { const apiUrl = process.env.NEXT_PUBLIC_API_URL; // "https://api.example.com" const dbUrl = process.env.DATABASE_URL; // undefined return <p>API: {apiUrl}</p>; } ``` Server Component бачить обидві змінні. Client Component - тільки публічну. Весь механізм у двох файлах. ### Реальний сценарій: інтеграція Stripe ```tsx // app/dashboard/page.tsx - Server Component import Stripe from 'stripe'; export default async function Dashboard() { const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); const prices = await stripe.prices.list(); return ( <CheckoutButton publishableKey={process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!} prices={prices.data} /> ); } // components/CheckoutButton.tsx 'use client'; export function CheckoutButton({ publishableKey, prices }) { // publishableKey прийшов як пропс - секрет не покинув сервер return <button>Оплатити</button>; } ``` Секретний ключ залишається на сервері. Публічний ключ потрапляє в браузер через `NEXT_PUBLIC_`. Це стандартний патерн у шаблоні Vercel Commerce. ### Edge case: ISR і рантайм vs збірка ```tsx // app/post/[id]/page.tsx export const revalidate = 3600; // ревалідація щогодини export default async function Post({ params }: { params: { id: string } }) { // process.env читається в *рантаймі* при кожній ревалідації const apiKey = process.env.API_KEY; const data = await fetch(`https://api.example.com/${params.id}`, { headers: { Authorization: `Bearer ${apiKey}` } }).then(r => r.json()); return <div>{data.title}</div>; } ``` Серверні змінні без префікса читаються в рантаймі, тому оновлення `API_KEY` у дашборді Vercel набере чинності при наступній ревалідації без нової збірки. Змінна з `NEXT_PUBLIC_` у тому ж місці все одно використовувала б старе значення зі збірки. Ця різниця важлива для ISR і edge деплоїв.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.