Skip to main content

Змінні середовища в Next.js

Змінні середовища в 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 деплоїв.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?