Змінні середовища в 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
Швидкий приклад
# .env.local
DATABASE_URL=postgresql://localhost:5432/mydb # тільки сервер
NEXT_PUBLIC_API_URL=https://api.example.com # доступно в браузері// 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_ статично замінюються у бандлі. Динамічний доступ не працює на клієнті:
// Працює
const url = process.env.NEXT_PUBLIC_API_URL;
// НЕ працює в браузері
const key = "NEXT_PUBLIC_API_URL";
const url = process.env[key]; // undefinedwebpack DefinePlugin замінює тільки літеральні посилання на process.env.NEXT_PUBLIC_*. Доступ через властивість об'єкта повертає undefined.
Типізація через 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 компоненті
'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 клієнтський компонент
// 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
// 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 збірка
// 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 деплоїв.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.