Next.js Route Handler для нейросети: стриминг ответа в App Router
Пишем app/api/chat/route.ts, который стримит ответ модели через OpenAI SDK, и клиентский компонент, который выводит текст по мере генерации. Ключ остаётся на сервере.
4 мин · · разработка
В App Router серверные эндпоинты пишутся как Route Handler — функции GET, POST и т. д. в файле route.ts. Если подключить к такому обработчику нейросеть со стримингом, пользователь видит ответ по мере генерации, а API-ключ не покидает сервер. Ниже — рабочий route.ts на OpenAI SDK, клиентский компонент чата и разбор ошибок, на которых такие маршруты обычно ломаются.
Установка и переменные окружения
npm install openaiФайл .env.local в корне проекта:
ZUKKO_API_KEY=fetch_ваш_ключ
Имя переменной без префикса NEXT_PUBLIC_, поэтому Next.js не встроит её в клиентский бандл — она доступна только серверному коду. Файл .env.local не коммитится. Ключ создаётся на странице API-ключи.
Next.js Route Handler со стримингом
Файл app/api/chat/route.ts:
import OpenAI from "openai";
export const runtime = "nodejs";
const client = new OpenAI({
apiKey: process.env.ZUKKO_API_KEY,
baseURL: "https://api.zukko.pro/v1",
});
type ChatMessage = { role: "user" | "assistant"; content: string };
const ERRORS: Record<number, string> = {
401: "Неверный API-ключ на сервере",
402: "На балансе недостаточно средств",Что важно в этом коде
- Базовый адрес —
https://api.zukko.pro/v1, ключ читается изprocess.env.ZUKKO_API_KEY. Клиент создаётся один раз на уровне модуля. - Байты, а не строки. Тело
Responseдолжно состоять изUint8Array, поэтому каждый кусок текста кодируется черезTextEncoder. Если положить в поток строку, Node.js выдаст ошибку о не-Uint8Array чанке. Так же устроен пример со стримингом в документации Next.js. - Пустые
choices. Последний чанк потока несёт толькоusageи пустой массивchoices. Строкаif (chunk.choices.length === 0) continue;пропускает его — без неё обращение кchunk.choices[0].deltaупадёт в самом конце ответа. close()иerror()вызываются по разным веткам. Вызовclose()послеerror()сам выбросит исключение.- Отмена. Если клиент закрыл вкладку или прервал
fetch, поток отменяется и срабатываетcancel(); он прерывает запрос к модели черезAbortController. - Ошибки до начала стрима (неверный ключ, нет баланса, модель не разрешена) возвращаются обычным JSON с тем же статусом — клиент проверяет
response.ok. Ошибка 503 с кодомmodel_disabledозначает, что модель выключена: возьмите доступную изGET https://api.zukko.pro/v1/models. messages.slice(-20)ограничивает историю: каждый запрос оплачивается вместе со всей переданной историей.content-type: text/plain. Мы отдаём сырой текст, а не события SSE, поэтомуtext/event-streamздесь был бы неверным.
Клиентский компонент
Файл app/chat.tsx:
"use client";
import { useState } from "react";
type Message = { role: "user" | "assistant"; content: string };
export default function Chat() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
async function send(e: React.FormEvent) {
e.preventDefault();
const text = input.trim();И подключите компонент на странице app/page.tsx:
import Chat from "./chat";
export default function Page() {
return <Chat />;
}Компонент сразу добавляет пустое сообщение ассистента и дописывает в него каждый пришедший кусок. Состояние обновляется без мутаций — через map с новым объектом для последнего сообщения, иначе в строгом режиме React текст может задвоиться.
Модели и параметры
Модель меняется в одной строке: claude-opus-5-5, gpt-5.6-sol, glm-5.3-flash и другие. Набор моделей меняется, актуальный список — GET https://api.zukko.pro/v1/models или страница «Модели» в кабинете. Какая модель лучше для вашего продукта, проверьте на своих диалогах.
С temperature и top_p будьте осторожны: новые модели Claude (claude-sonnet-5, claude-opus-5-5) могут отклонить temperature, отличную от 1, и top_p ниже 0.99 с ошибкой 400. Для них эти параметры лучше не передавать. Для GPT и GLM их можно указывать как обычно:
const stream = await client.chat.completions.create({
model: "gpt-5.6-sol",
messages,
stream: true,
temperature: 0.7,
max_tokens: 2048,
});Расход можно оценить так: при 2 000 входных и 500 выходных токенах на claude-sonnet-5 запрос стоит (2 000 × 0.3 + 500 × 0.3) / 1 000 000 = $0.00075. Полную картину по ключам смотрите в разделе «Использование» кабинета, а лимиты на ключ — в документации по лимитам. Формат запроса описан в справке по chat completions.
Частые вопросы
Нужен ли Anthropic SDK для моделей Claude?
Нет. Через OpenAI-совместимый формат доступны все модели: Claude, GPT, GLM — шлюз конвертирует запрос сам. Если вам всё же нужен Anthropic SDK, базовый адрес для него — https://api.zukko.pro без /v1; подробнее — в статье о выборе формата.
Можно ли использовать Vercel AI SDK?
Да, документация Next.js упоминает его как готовую обёртку для стриминга. Подключайте его через OpenAI-совместимого провайдера с базовым адресом https://api.zukko.pro/v1, а точные названия опций сверяйте с документацией AI SDK. Код выше показывает, что и без библиотеки стриминг занимает несколько десятков строк.
Почему ответ приходит целиком, а не по частям?
Проверьте, что в запросе стоит stream: true, а между клиентом и сервером нет буферизующего прокси. При деплое на собственный сервер за nginx отключите буферизацию для этого маршрута.
Может ли ключ утечь в браузер?
Нет, если переменная не начинается с NEXT_PUBLIC_ и используется только в route.ts. Браузер видит лишь запрос к /api/chat. Для надёжности заведите для сайта отдельный ключ с дневным лимитом.
zukko
Платите в 25×
меньше
за Claude Code & ChatGPT
Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.
Похожие статьи
- Промпт-инъекция: что это и как защитить бота и агентаОбъясняем, как работает промпт-инъекция через пользовательский ввод и чужие документы, и какие меры снижают ущерб: изоляция данных, права инструментов, лимиты ключа.
- Свой прокси к нейросети на Node.js: не показывать API-ключ во фронтендеНебольшой сервер на Express между вашим фронтендом и API нейросети: ключ остаётся на сервере, а модели, длина запроса и частота ограничены.
- DeepSeek API на Python: запросы, стриминг, JSON и обработка ошибокГотовый код для DeepSeek V4.1 Flash и V4 Pro на OpenAI SDK: запрос, стриминг, разбор JSON и аккуратная обработка ошибок.
- Промпт-инжиниринг: примеры и 12 готовых шаблонов для работыДвенадцать шаблонов промптов для рабочих задач: что в них написано, почему именно так и сколько стоит один вызов по API.