Свой прокси к нейросети на Node.js: не показывать API-ключ во фронтенде
Небольшой сервер на Express между вашим фронтендом и API нейросети: ключ остаётся на сервере, а модели, длина запроса и частота ограничены.
4 мин · · разработка · API-ключи
Если вызвать API нейросети прямо из браузера, ключ окажется в коде страницы или во вкладке Network, и любой посетитель сможет его скопировать. Решение стандартное: поставить между фронтендом и API свой серверный слой. Ниже небольшой Node.js OpenAI proxy на Express: ключ лежит в переменной окружения, список моделей и длина запроса ограничены, ответ идёт потоком, а частота запросов считается по пользователю. Тот же приём работает и для моделей Gemini (например, gemini-3.8-flash): меняется только строка с моделью.
Зачем нужен прокси
Ключ во фронтенде нельзя спрятать: минификация и обфускация не помогают, ведь запрос к API всё равно видно в браузере. Серверный прокси решает сразу четыре задачи:
- ключ остаётся на сервере и не попадает к пользователям;
- вы сами решаете, какие модели доступны и какой размер запроса допустим;
- можно ограничить частоту запросов на пользователя и привязать расход к аккаунту;
- системный промпт хранится на сервере и не раскрывается клиенту.
Общие правила хранения ключей собраны в статье про безопасность API-ключей.
Подготовка
mkdir ai-proxy && cd ai-proxy
npm init -y
npm install express openaiВ package.json добавьте "type": "module". Нужен Node.js 18 или новее. Создайте отдельный ключ под этот проект в кабинете Zukko и сразу задайте ему дневной лимит в долларах и список разрешённых моделей: это последняя линия защиты, если что-то пойдёт не так в коде. Как это сделать, описано в статье про лимит на ключ.
export ZUKKO_API_KEY="fetch_ключ_этого_проекта"Код сервера
// server.js
import express from "express";
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.zukko.pro/v1",
apiKey: process.env.ZUKKO_API_KEY,
});
const ALLOWED_MODELS = new Set(["deepseek-v4.1-flash", "claude-sonnet-5-5"]);
const MAX_MESSAGES = 20;
const MAX_CHARS = 8000;
const MAX_OUTPUT_TOKENS = 800;
const WINDOW_MS = 60_000;Обратите внимание на три детали. Во-первых, роль system приходит только с сервера, а от клиента принимаются лишь user и assistant: иначе посетитель подставит свой системный промпт. Во-вторых, chunk.choices?.[0] с проверкой нужна потому, что последний чанк потока приходит с пустым списком choices. В-третьих, при закрытии соединения клиентом запрос к API отменяется, и генерация не продолжается впустую; токены, сгенерированные до обрыва, всё равно могут быть списаны.
Как читать поток во фронтенде
EventSource не умеет отправлять POST, поэтому используйте fetch и читайте тело как поток:
const resp = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: "Bearer " + userToken },
body: JSON.stringify({ model: "deepseek-v4.1-flash", messages }),
});
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const events = buffer.split("\n\n");
buffer = events.pop();
for (const ev of events) {
const data = ev.replace(/^data: /, "");
if (data === "[DONE]") continue;
const { delta } = JSON.parse(data);
if (delta) output.textContent += delta;
}
}Для Next.js то же самое делается в Route Handler. Если нужен разбор стриминга на Express подробнее, он есть в статье про стриминг в Express.
Сколько может стоить злоупотребление
Посчитаем худший случай при ограничениях из кода. Лимит в коде задан в символах, поэтому возьмём с запасом (условно) 8000 входных токенов и 800 выходных. Цены на 18 октября 2026, $ за 1M токенов.
deepseek-v4.1-flash(0.013 / 0.038): 8000 × 0.013 / 1M + 800 × 0.038 / 1M = 0.000104 + 0.0000304 = около $0.00013.claude-sonnet-5-5(0.4 / 2): 8000 × 0.4 / 1M + 800 × 2 / 1M = 0.0032 + 0.0016 = $0.0048.
Лимит 10 запросов в минуту даёт до 600 запросов в час на одного пользователя. На Sonnet это 600 × 0.0048 = $2.88 в час, на V4.1 Flash около $0.08. Отсюда два вывода: дорогие модели стоит открывать только авторизованным пользователям, а дневной лимит на самом ключе обязателен, потому что счётчик в памяти сбрасывается при перезапуске процесса и не работает при нескольких экземплярах сервера. Для нескольких экземпляров вынесите счётчик в Redis или другую общую базу.
Что ещё добавить
- Ограничение источников (CORS) и HTTPS на стороне сервера.
- Журнал: id пользователя, модель,
usageиз ответа. Не логируйте тексты диалогов без необходимости. - Явные сообщения об ошибках для пользователя: при ответе 402 от API (не хватает баланса) покажите «сервис временно недоступен», а не сырой текст. Основные коды шлюза: 401 ключ, 402 баланс, 403 модель не разрешена ключу, 429 лимит ключа или провайдера, 503 провайдер недоступен или модель выключена.
- Тот же сервер можно использовать для разных моделей, потому что они доступны по одному адресу и в одном формате.
Частые вопросы
Можно ли просто спрятать ключ в коде фронтенда?
Нет. Всё, что отправляется в браузер, пользователь может прочитать. Единственный надёжный путь: ключ только на сервере.
Подойдёт ли это для Gemini и других моделей?
Да. Прокси передаёт строку model, а шлюз принимает модели разных поставщиков в одном формате. Добавьте нужное имя, например gemini-3.8-flash, в ALLOWED_MODELS.
Почему не использовать только лимиты ключа?
Лимиты ключа защищают бюджет целиком, но не отличают одного пользователя от другого: один злоумышленник может исчерпать дневной лимит для всех. Поэтому нужны оба уровня: счётчик на пользователя и лимит на ключе.
Что делать, если ключ всё же утёк?
Создайте новый ключ, удалите старый и проверьте историю расходов. Порядок действий описан в статье про утечку ключа.
Чтобы запустить прокси, создайте ключ в кабинете Zukko, задайте ему лимиты и подставьте в переменную окружения. Оплата: СБП, криптовалюта или карты MasterCard.
zukko
Платите в 25×
меньше
за Claude Code & ChatGPT
Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.
Похожие статьи
- DeepSeek API на Python: запросы, стриминг, JSON и обработка ошибокГотовый код для DeepSeek V4.1 Flash и V4 Pro на OpenAI SDK: запрос, стриминг, разбор JSON и аккуратная обработка ошибок.
- Gemini API на Python: запросы через OpenAI SDK и OpenAI-форматРабочий пример Gemini API на Python без отдельной библиотеки: меняем base_url в OpenAI SDK, добавляем стриминг и JSON и сравниваем с google-genai.
- Anthropic SDK для Python: base_url, ключ и первый запрос через шлюзОфициальный пакет anthropic для Python нужно только направить на другой адрес. Разбираем base_url, ключ, стриминг, подсчёт токенов, кэш и ошибки.
- Безопасность Claude Code: чек-лист перед запуском агента в проектеАгент читает файлы и запускает команды, поэтому настройки безопасности нужны до первого запуска. Чек-лист: разрешения, секреты, песочница, отдельный ключ с лимитом.