Как управлять переменными окружения в Next.js? В чём разница между серверными и клиентскими переменными?
Переменные без префикса NEXT_PUBLIC_ доступны только на сервере; с префиксом — встраиваются в клиентский бандл при сборке. Секреты никогда не добавляют NEXT_PUBLIC_.
Переменные окружения в Next.js
Next.js читает переменные из файлов .env.local, .env.development, .env.production и .env. Приоритет: .env.local > .env.development/.env.production > .env.
Серверные переменные
Любая переменная без префикса NEXT_PUBLIC_ доступна только на сервере — в Server Components, Route Handlers, Server Actions, getServerSideProps и getStaticProps. В браузер она не попадает: Next.js просто не включает её в клиентский бандл.
# .env.local
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
SECRET_API_KEY=supersecret123
NEXT_PUBLIC_APP_URL=https://example.com
// app/api/users/route.ts — серверный код, DATABASE_URL доступна
import { NextResponse } from 'next/server';
export async function GET() {
const dbUrl = process.env.DATABASE_URL; // OK
const appUrl = process.env.NEXT_PUBLIC_APP_URL; // тоже доступна на сервере
return NextResponse.json({ appUrl });
}
Клиентские переменные (NEXT_PUBLIC_)
Переменные с префиксом NEXT_PUBLIC_ встраиваются в JS-бандл на этапе сборки через статическую подстановку (inlining). Обращение process.env.NEXT_PUBLIC_APP_URL превращается буквально в строку "https://example.com" при компиляции.
// components/Footer.tsx — Client Component
'use client';
export function Footer() {
// process.env.NEXT_PUBLIC_APP_URL встроена в бандл
return <footer>Сайт: {process.env.NEXT_PUBLIC_APP_URL}</footer>;
}
Переопределение на этапе выполнения (runtime env)
Если нужна переменная, известная только в runtime (например, при деплое в Kubernetes), используйте next.config.ts с serverRuntimeConfig / publicRuntimeConfig или передавайте значения через серверный компонент в клиентский через пропсы.
// next.config.ts
import type { NextConfig } from 'next';
const config: NextConfig = {
serverRuntimeConfig: {
mySecret: process.env.MY_SECRET,
},
publicRuntimeConfig: {
apiUrl: process.env.NEXT_PUBLIC_API_URL,
},
};
export default config;
// lib/config.ts
import getConfig from 'next/config';
const { serverRuntimeConfig, publicRuntimeConfig } = getConfig();
export const apiUrl = publicRuntimeConfig.apiUrl as string;
export const secret = serverRuntimeConfig.mySecret as string; // только сервер
Типизация переменных окружения
Создайте env.d.ts в корне проекта, чтобы TypeScript знал о ваших переменных:
// env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string;
SECRET_API_KEY: string;
NEXT_PUBLIC_APP_URL: string;
}
}
Подводные камни
- Секрет без префикса попал в Client Component — Next.js выбросит ошибку на этапе сборки или просто вернёт
undefined, но секрет не утечёт. Тем не менее логика сломается молча. - Динамическое обращение к
process.env— конструкцияprocess.env[key]с переменнойkeyне работает дляNEXT_PUBLIC_: подстановка статическая, парсер ищет именно литералprocess.env.NEXT_PUBLIC_XXX. - Изменение
.env.localтребует рестарта — hot reload не подхватывает новые значения без перезапускаnext dev. .env.localне должен коммититься — добавьте его в.gitignore. Добавляйте только.env.exampleс заглушками.- Порядок файлов —
.env.localимеет наивысший приоритет и перекрывает все остальные файлы, что может сбивать с толку в CI/CD где.env.localотсутствует. - Пустая строка vs отсутствие переменной —
DATABASE_URL=и отсутствие строки — разные вещи: в первом случаеprocess.env.DATABASE_URL === "", во втором —undefined. - Не используйте
publicRuntimeConfigс App Router — он работает только с Pages Router; в App Router передавайте публичные значения через серверный компонент или переменныеNEXT_PUBLIC_.
Common mistakes
- Класть секрет с префиксом
NEXT_PUBLIC_ - Полагать, что
process.env.Xв клиентском файле — runtime - Коммитить
.env.localили.env.production - Менять секрет в проде без перезапуска процесса
What the interviewer is testing
- Понимает, что только
NEXT_PUBLIC_*идут в бандл - Знает порядок загрузки env-файлов
- Использует
server-onlyдля критичных модулей - Различает build-time inline и runtime read