EvolCode
треки/API и .env·04 / 05
7 мин чтения

Серверные vs клиентские переменные — главная ловушка

В Next.js (и Vite) переменные делятся на два мира: те, что видит сервер, и те, что видит браузер. Граница — префикс. Тот, кто этого не знает, рано или поздно сольёт ключ. Тот, кто знает, спит спокойно.

Почему вообще есть две стороны

Современный фреймворк собирает один проект в две части. Серверный код выполняется в Node.js на вашем сервере — он может прочитать любой файл, любую переменную. Клиентский код собирается в JS-бандл и улетает в браузер пользователя — там доступно только то, что мы туда специально положили. Переменная без префикса попадает только на сервер. С префиксом — попадает В БАНДЛ, и любой пользователь может открыть DevTools → Sources и прочитать её значение.

Префиксы в разных фреймворках

Next.js: NEXT_PUBLIC_X — клиент видит, X без префикса — только сервер. Vite: VITE_X — клиент, X — только сервер. Create React App (legacy): REACT_APP_X. Astro: PUBLIC_X. Логика везде одна — спецпрефикс прокидывает в бандл, без префикса значение остаётся на сервере. Уберите префикс — и ваш Client Component получит undefined.

Что МОЖНО под публичным префиксом

Всё, что и так публично доступно. Адрес вашего же API (

$ NEXT_PUBLIC_API_URL=https://api.example.com

). ID Google Analytics. Stripe Publishable Key (pk_...) — он специально для клиента. Sentry DSN. Всё, что любой пользователь и так увидит при работе с сайтом.

Что НЕЛЬЗЯ — даже случайно

Любые sk_.... Пароли БД. JWT-секреты. SMTP-пароли. Stripe Secret Key. AWS Access Key. Если вы в момент написания подумали «а удобно бы было дёрнуть OpenAI прямо из клиента» — остановитесь. Сделайте серверный API-роут, и пусть клиент дёргает его, а роут уже идёт в OpenAI с серверным ключом. Это не пара лишних строк — это разница между «работает» и «ваш ключ в репо у школьника».

примерtsx
// ПЛОХО: ключ зашит в клиентский компонент.
// Любой нажмёт F12 → Sources → увидит твой ключ.

// app/page.tsx (Client Component)
'use client';
export default function Page() {
  const callApi = async () => {
    // ↓ КЛЮЧ ЛЕТИТ В БРАУЗЕР, ЕГО ВИДНО ВСЕМ
    await fetch('https://api.openai.com/v1/chat/completions', {
      headers: { Authorization: 'Bearer sk-proj-...' },
    });
  };
  return <button onClick={callApi}>Спросить ИИ</button>;
}

// ХОРОШО: ключ остаётся на сервере, клиент дёргает свой роут.

// app/api/ask/route.ts (серверная функция)
export async function POST(req: Request) {
  const { prompt } = await req.json();
  const r = await fetch('https://api.openai.com/v1/chat/completions', {
    headers: {
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
      'Content-Type': 'application/json',
    },
    method: 'POST',
    body: JSON.stringify({
      model: 'gpt-4o-mini',
      messages: [{ role: 'user', content: prompt }],
    }),
  });
  return Response.json(await r.json());
}

// app/page.tsx — клиент дёргает СВОЙ /api/ask, не OpenAI напрямую
'use client';
export default function Page() {
  const ask = async () => {
    await fetch('/api/ask', {
      method: 'POST',
      body: JSON.stringify({ prompt: 'Привет' }),
    });
  };
  return <button onClick={ask}>Спросить</button>;
}

Антипаттерн vs правильный подход — серверный роут как прокси

главное
  • 01NEXT_PUBLIC_ (Next), VITE_ (Vite), PUBLIC_ (Astro) — переменная попадает в браузер
  • 02Без префикса — только сервер; в Client Component вернёт undefined
  • 03Любой sk_, password, secret — НИКОГДА не давать публичный префикс
  • 04Если клиенту нужен сервис с секретным ключом — делаем серверный роут-прокси
  • 05pk_ (Stripe), Sentry DSN, GA ID — публичные, можно с префиксом
  • 06Проверка перед пушем: открыть бандл (DevTools → Sources) и поискать sk_
проверь себя
01Вы написали `NEXT_PUBLIC_OPENAI_KEY=sk-proj-...` в .env. Что произойдёт?
02Client Component в Next.js пытается прочитать `process.env.OPENAI_API_KEY` (без префикса). Что вернётся?
03Stripe даёт два ключа: pk_test_... и sk_test_.... Какой включать в Stripe.js на странице чекаута?
следующий урок: Подключаем первый API: пошагово