Перейти к содержимому
Поиск
Разработка

Hono: HTTP API на Node.js — мастерская и 9 практических опытов

Девять практических опытов на настоящем Hono: заявки, JSON, ошибки, middleware, страницы, доступ и поток прогресса. Мастерская и исходники Node.js для Windows/Linux.

Александр Некрасов
Hono — маршруты HTTP API, middleware и потоковые ответы

Hono на практике: HTTP API для интерфейса, девять живых опытов, JSON и права, middleware, поток прогресса, Node.js и развёртывание Windows/Linux.

Hono: небольшой API, который обслуживает настоящий интерфейс

У формы есть поля, у таблицы есть фильтры, а у кнопки «Создать» должен появиться понятный результат. Между интерфейсом и данными нужен HTTP API: принять запрос, проверить значения, определить пользователя, выполнить действие и вернуть ответ. Hono помогает написать этот слой на JavaScript без большого количества служебного кода. В этой статье мы собираем учебный сервис заявок и разбираем каждую часть его поведения.

Вы сможете открыть карточку по номеру, создать заявку, исправить ошибки формы, пролистать результаты, увидеть запрет доступа и получить прогресс порциями. Это девять связанных ситуаций, а не девять одинаковых кнопок с разным текстом. После статьи останется скачиваемый проект: общий router, браузерная мастерская, настоящий Node HTTP-сервер, тесты и примеры конфигурации Linux-сервера.

Мастерская использует настоящий Hono, собранный в локальный JavaScript. Она вызывает app.request прямо в браузере. Поэтому можно безопасно экспериментировать с вымышленными заявками без отправки введённого JSON на публичный Node-сервис. В архиве тот же обработчик подключён к реальному HTTP через адаптер Node.js. Разницу между этими режимами мы будем учитывать при каждом измерении и проверке.

Где применять: админки, формы, интеграции и небольшие сервисы

Практичный первый сценарий — backend для собственной админки. Таблица запрашивает список заказов, карточка получает одну запись, форма отправляет изменения. Hono определяет маршрут и формат ответа, а ваше приложение решает, где лежат данные и кто имеет право их менять. Такой подход удобен, когда интерфейс на Vue, React или обычном JavaScript уже существует и ему нужен компактный самостоятельный API.

Другой сценарий — промежуточный сервис для интеграции. Он принимает запрос от вашего приложения, проверяет его, обращается к разрешённой внешней системе и преобразует результат в стабильный контракт. В рабочем проекте здесь понадобятся таймауты, обработка отказов и секреты на сервере. Нельзя превращать подобный маршрут в открытый прокси, которому посетитель передаёт любой адрес для загрузки.

Ещё Hono подходит для webhook-приёмников, служебных endpoint, генерации небольших отчётов, чтения статусов фоновой обработки. Он не добавляет базу, очередь или панель управления автоматически. Если нужен полный набор готовых прикладных модулей, выбор фреймворка стоит делать по задачам команды. Здесь мы выбираем Hono ради прозрачного HTTP-слоя, который легко читать, проверять и подключать к существующему приложению.

Версии, Node.js и границы совместимости

Проект закреплён на Hono 4.13.18, адаптере @hono/node-server 2.1.7 и Bootstrap 5.3.9. Версии проверены при подготовке материала 11 ноября 2026 года. Для воспроизведения используется Node.js 24 LTS, не ниже 24.15. Это требования данного учебного проекта и его проверок; не следует автоматически переносить их на все старые выпуски Hono.

В архиве находится package-lock.json, поэтому установка через npm ci восстанавливает закреплённое дерево зависимостей. Обновление выполняйте отдельно: измените версии, пересоберите приложение и повторите тесты. Незаметная замена пакета в уже опубликованном примере затрудняет поиск различий между результатом читателя и описанием статьи.

Основной сайт документации уже содержит раздел перехода на следующую major-версию. Для спорных деталей сверяйтесь с исходниками закреплённого тега v4.13.18. Поддержка разных сред выполнения не означает, что Node, браузер и облачная платформа одинаково работают с файловой системой, соединениями и жизненным циклом процесса. Маршруты можно разделить, адаптер и развёртывание всё равно проверяют отдельно.

node --version
npm ci
npm run build
npm test
npm start

Мини-мастерская: девять опытов и свои запросы

Выберите опыт, нажмите один из готовых вариантов и выполните запрос. В поле пути можно менять номер записи, параметры страницы, строку поиска и число событий потока. Для POST доступно тело JSON. Рядом показан фактический размер в байтах UTF-8: кириллица позволяет сразу увидеть, почему число символов и размер HTTP-тела различаются.

Ответ содержит статус, заголовки, тело и журнал middleware. Ошибки 400, 401, 403, 404, 413, 422 и намеренная 500 здесь являются частью обучения. Их появление означает, что соответствующая ветка API действительно отработала. Кнопка скачивания сохраняет результат со сведениями о режиме выполнения. Исходник текущего запроса можно скачать отдельно и запустить внутри распакованного проекта.

Для знакомства пройдите такой путь: найдите заявку, создайте новую, найдите её в общем списке, попробуйте отрицательный бюджет, затем проверьте чужую запись и остановите поток прогресса. «Сбросить всё» вернёт начальные восемнадцать заявок в браузерной мастерской. Серверный экземпляр из архива имеет собственную память и сбрасывается отдельно при перезапуске Node.

На странице работает Hono через app.request в памяти браузера. Это не сетевой вызов публичного API. В архиве есть настоящий Node HTTP-сервер с теми же маршрутами, локальные зависимости, тесты и инструкции Windows/Linux.

Как запрос проходит через приложение

У запроса есть метод, адрес, заголовки и необязательное тело. Router сопоставляет метод и путь с обработчиком. Middleware выполняют общие действия: создают идентификатор, применяют политику CORS, ограничивают тело, проверяют учебную идентичность. Обработчик получает данные через Context и возвращает Response. Затем управление проходит через завершающие части middleware.

В нашем проекте интерфейс не выбирает JavaScript-функцию по имени и не выполняет присланный код. Он создаёт ограниченный запрос на путь /api/…. Этот запрос проходит через ту же маршрутизацию, что и обращение к Node-серверу. Такой способ позволяет исследовать реальное поведение framework, оставаясь внутри небольшой учебной области.

Важное разделение: HTTP-контракт описывает, что клиент может отправить и что получит обратно; транспорт определяет, как эти данные дошли до приложения. app.request проверяет первый слой. Ошибки DNS, TLS, firewall, балансировщика и сетевых таймаутов появляются на втором. Поэтому успешный браузерный опыт ещё не подтверждает правильность вашего VPS или публичного домена.

import { createApi } from './src/router.js';
const app = createApi();
const response = await app.request('/api/jobs/1');
console.log(response.status);
console.log(await response.json());

Опыт 1. Каталог и карточка: параметры пути

Маршрут GET /api/jobs/:id обслуживает карточку заявки. Двоеточие обозначает параметр пути: адрес заканчивается номером, а обработчик получает его через c.req.param('id'). Это удобно для отдельной страницы записи, боковой панели просмотра или диалога редактирования внутри списка. Разметка интерфейса может меняться, контракт карточки остаётся прежним.

Параметр приходит строкой. Наш обработчик сначала проверяет форму положительного целого числа, затем ищет запись. Неверный формат возвращает 422, корректный номер отсутствующей заявки — 404. Такое различие помогает клиенту понять, что произошло: он сформировал неправильный адрес или запись действительно не существует. Измените номер 1 на 999 и затем на abc, чтобы сравнить ответы.

В реальном приложении поиск обычно выполняется через репозиторий или базу данных. Важно не подставлять параметр в SQL конкатенацией и не считать существование записи доказательством доступа к ней. В мастерской общий каталог намеренно открытый и синтетический; отдельный закрытый маршрут ниже показывает проверку владельца. Синтаксис маршрутов описан в официальном разделе Routing.

app.get('/api/jobs/:id', (c) => {
  const id = c.req.param('id');
  // Проверить формат id, найти запись, проверить доступ.
  return c.json({ ok: true, requestedId: id });
});

Опыт 2. Создать заявку: JSON и ответ 201

Вторая задача показывает путь от формы до новой записи. Клиент отправляет POST /api/jobs с заголовком application/json и объектом из двух полей: title и budget. При успехе API возвращает 201, созданную заявку и заголовок Location с адресом карточки. Интерфейс может добавить её в список, открыть подробности или показать подтверждение без повторного разбора всех данных.

Поле owner пользователь не присылает. В демо оно назначается приложением, а в рабочем сервисе его нужно брать из проверенной серверной идентичности. Иначе пользователь смог бы создать запись от имени другого человека, просто изменив JSON. Статус новой заявки также назначает сервер: клиентская форма не является доверенным источником внутренних полей.

Запись действительно попадает в массив этого экземпляра приложения. Откройте затем каталог или карточку по возвращённому номеру. Всего разрешено пятьдесят записей, чтобы эксперимент оставался ограниченным. После достижения лимита возвращается 409 с подсказкой о сбросе. Это учебная политика памяти, а не универсальный предел Hono и не замена постоянному хранилищу.

const response = await fetch('http://127.0.0.1:8502/api/jobs', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ title: 'Настроить API сайта', budget: 15000 })
});
console.log(response.status, await response.json());

Валидация: отличить синтаксис JSON от ошибок полей

Сначала API проверяет тип содержимого. Текст с неподходящим Content-Type получает 415. Затем выполняется разбор JSON: лишняя запятая или незакрытая скобка дают 400. После успешного разбора начинается прикладная проверка. Название должно содержать от трёх до восьмидесяти символов, бюджет — быть целым числом от нуля до миллиона, неизвестные поля запрещены. Нарушения этих правил возвращают 422.

Ответ с полями ошибок удобен для формы: сообщение title показывается рядом с названием, budget — рядом с бюджетом. Пользователю не нужно угадывать причину общего текста «Что-то пошло не так». При этом технический код ошибки остаётся стабильным, а человеческое сообщение можно перевести или переписать. Клиенту лучше опираться на code и fields, а не искать слова в русской фразе.

Небольшая ручная проверка здесь сделана специально, чтобы путь данных был виден целиком. В большом контракте удобно подключить Zod или Ajv и JSON Schema. Библиотека структуры не заменяет проверку права на операцию, уникальности записи или доступного остатка на складе.

{
  "ok": false,
  "error": {
    "code": "VALIDATION",
    "message": "Исправьте поля заявки.",
    "fields": { "budget": "Бюджет: целое число от 0 до 1 000 000." }
  },
  "requestId": "идентификатор конкретного запроса"
}

Опыт 3. Middleware: общий порядок до и после

Middleware полезны для действий, которые должны одинаково выполняться в разных маршрутах. В мастерской внешний слой создаёт requestId и начинает измерение, внутренний отмечает вход в журнал, затем вызывается обработчик. После него внутренний слой завершает журнал, внешний добавляет заголовки. Сначала выполняется код перед await next, затем обработчик, затем части после next в обратном порядке.

Откройте опыт middleware и выполните health-запрос. В журнале видно пять последовательных отметок. Это не нарисованная заранее схема: события создаются теми функциями, которые обрабатывают выбранный запрос. Для ответа с ранним отказом последовательность меняется, поскольку обработчик может вообще не получить управление. Например, проверка тела или идентичности способна вернуть ответ раньше бизнес-операции.

Регистрация и область пути имеют значение. Авторизацию закрытой группы нужно подключить до её обработчиков, а общие заголовки — так, чтобы они попали и в ошибочные ответы. Не стоит бездумно навешивать тяжёлую проверку на каждую статическую картинку. Общий механизм и порядок подробно описаны в документации Middleware.

app.use('/api/*', async (c, next) => {
  const started = performance.now();
  await next();
  c.header('Server-Timing', `app;dur=${performance.now() - started}`);
});

Идентификатор запроса и измерение времени

RequestId связывает сообщение пользователя с записью в серверном журнале. Человек сообщает, что операция не удалась, прикладывает идентификатор, а разработчик находит конкретную обработку. В нашем приложении UUID создаётся заново для каждого запроса и возвращается в заголовке X-Request-Id. Ошибочный JSON также содержит это значение, чтобы его было удобно сохранить вместе с описанием проблемы.

Показываемая длительность измеряется на том устройстве, где выполняется мастерская. Это полезно для понимания последовательности, но не является сравнительным тестом фреймворков. Здесь нет сетевой задержки до сервера, выполнения SQL или реального внешнего API. Задержка событий SSE, наоборот, задана специально, чтобы посетитель успел увидеть отдельные шаги и нажать остановку.

Server-Timing внешнего middleware измеряет подготовку Response. Поток может продолжать отправляться после того, как этот этап завершился. Поэтому маленькое значение заголовка и заметная длительность чтения SSE не противоречат друг другу. В производственной диагностике полезно отдельно измерять всю операцию, обращение к базе, внешние вызовы и отправку результата, не смешивая их в одну якобы универсальную скорость.

Опыт 4. Общий формат 404 и внутренних ошибок

Для клиента важно получать предсказуемую структуру даже тогда, когда операция закончилась неуспешно. В проекте используется объект с ok, error и requestId. У ошибки есть короткий code и понятное message. Неизвестный адрес обрабатывается через notFound, а неожиданное исключение — через onError. Намеренный маршрут failure позволяет проверить эту ветку без настоящей поломки окружения.

Внутренний текст исключения не отправляется посетителю. В Node-версии он попадает в журнал вместе с requestId; клиент получает нейтральное сообщение и 500. Так интерфейс может предложить повторить действие или передать номер обращения, а разработчик сохранит сведения для диагностики. Тело запроса, Bearer-токены и персональные данные в универсальный журнал добавлять не нужно.

Не всякая ошибка должна превращаться в 500. Неверный бюджет, отсутствие записи и запрет доступа — ожидаемые результаты контракта. Их лучше обработать там, где известен смысл ситуации. Общий обработчик нужен для действительно неожиданного сбоя. После начала отправки потокового ответа статус уже не получится произвольно заменить: ошибки потока требуют собственного сообщения и корректного закрытия.

app.notFound((c) => c.json({
  ok: false, error: { code: 'ROUTE_NOT_FOUND', message: 'Маршрут не найден.' }
}, 404));
app.onError((error, c) => {
  // Полные детали записываются только во внутренний журнал.
  return c.json({ ok: false, error: { code: 'INTERNAL' } }, 500);
});

Опыт 5. Пагинация, фильтры и пустое состояние

Админке редко нужен весь набор записей одним ответом. Маршрут списка принимает page, limit, q и status. Он фильтрует данные, выбирает страницу и возвращает metadata: общее число результатов, число страниц и номер следующей. Интерфейс на основе этих значений показывает счётчик, кнопки перехода и сообщение об отсутствии совпадений. В примере размер страницы ограничен десятью записями.

Попробуйте первую и вторую страницы открытых заявок, затем задайте несуществующее слово. Пустой массив с успешным статусом — нормальный результат поиска. Его не нужно превращать в 404 всей страницы. Если человек вводит limit=1000 или отрицательный номер страницы, API отвечает 422, потому что такой запрос нарушает договорённые границы. Удобная форма должна объяснять допустимые значения ещё до отправки.

Для большого изменяющегося набора данных простая пагинация по номеру имеет ограничения: добавление записей между запросами может сдвигать результаты. Тогда рассматривают курсор и стабильный порядок сортировки. Мастерская намеренно использует маленький массив, чтобы показать контракт без SQL. При переносе в базу фильтрация и выборка должны выполняться на стороне хранилища, а не после загрузки миллионов строк в память Node.

GET /api/jobs?page=2&limit=3&status=open

{
  "ok": true,
  "data": ["заявки текущей страницы"],
  "meta": { "page": 2, "limit": 3, "total": 12, "pages": 4, "next": 3 }
}

Опыт 6. Учебная идентичность и доступ к записи

Закрытый маршрут требует заголовок Authorization с открытой строкой demo-anna. Без неё возвращается 401: приложение не получило ожидаемую учебную идентичность. С этой строкой запрос к первой заявке успешен, а ко второй даёт 403, потому что владельцем второй является другой персонаж. Получить идентичность пользователя и проверить его право на конкретную запись — два разных этапа.

Такая демонстрация помогает понять распространённую ошибку API. Недостаточно проверить наличие входа в систему, если далее любой пользователь может подставить чужой номер в адрес. Проверку владельца, организации или роли необходимо выполнять для запрашиваемого ресурса. В некоторых продуктах вместо 403 возвращают 404, чтобы не раскрывать существование чужой записи; это отдельная политика контракта.

Внимание к границе примера: demo-anna опубликована в исходниках и известна всем. Она не защищает настоящую информацию. Для рабочего проекта потребуется проверенная серверная сессия или валидируемый токен, управление сроком действия и выходом, а также права на весь набор маршрутов. Общий каталог и создание в нашем демо намеренно открыты: там только синтетические данные.

curl -i   -H 'Authorization: Bearer demo-anna'   http://127.0.0.1:8502/api/private/jobs/1

# Тот же пользователь, но чужая запись: ожидается 403.
curl -i   -H 'Authorization: Bearer demo-anna'   http://127.0.0.1:8502/api/private/jobs/2

Опыт 7. Размер тела: ограничение до разбора

Тело запроса ограничено 1024 байтами. Это небольшой учебный предел, позволяющий увидеть 413 без загрузки больших файлов. Пресет отправляет длинное название на кириллице. Подсказка рядом с редактором показывает размер UTF-8, а middleware отклоняет запрос до обычной проверки полей. Ограничение длины названия и ограничение размера всего тела решают разные задачи и нужны независимо.

В Hono для этой задачи есть bodyLimit. В документации объясняется обработка Content-Length и чтение потока при его отсутствии. Наши программные проверки отдельно отправляют реальное тело без указанной длины. На настоящем HTTP корректность границ сообщения дополнительно обеспечивает адаптер и сервер. Не стоит воспринимать произвольный заголовок из доверенного in-memory вызова как доказательство размера неизвестного потока.

Для публичного сервиса нужен согласованный предел и на reverse proxy. В конфигурациях архива прокси допускает восемь килобайт, а API — один: небольшое превышение получает понятный JSON приложения, слишком большой запрос может быть остановлен раньше самим прокси. Формат такого ответа будет отличаться. Детали встроенного механизма приведены в Body Limit Middleware.

import { bodyLimit } from 'hono/body-limit';
app.use('/api/*', bodyLimit({
  maxSize: 1024,
  onError: (c) => c.json({
    ok: false, error: { code: 'BODY_TOO_LARGE' }
  }, 413)
}));

CORS: что показывает preflight и чего он не защищает

Если frontend и API находятся на разных origin, браузеру нужна политика доступа к ответу. В демонстрации разрешён точный origin https://demo.example, методы GET и POST, а также необходимые заголовки. Пресет OPTIONS показывает preflight-ответ middleware. Это позволяет увидеть, какие заголовки появятся и почему обычный обработчик создания заявки при таком запросе не запускается.

Сам app.request не применяет браузерные сетевые ограничения CORS. Он создаёт ответ в памяти и позволяет прочитать заголовки напрямую. Проверить реальное поведение браузера можно только отдельным сетевым вызовом с другого origin, когда участвуют сервер и политика браузера. Нельзя объявить CORS полностью проверенным лишь потому, что в нашей мастерской появился Access-Control-Allow-Origin.

CORS не заменяет авторизацию. curl, другой сервер и некоторые клиенты вообще не обязаны следовать браузерной политике чтения. Если операция запрещена человеку, она должна быть запрещена прикладной проверкой независимо от Origin. Для собственного frontend замените демонстрационный домен точным адресом со схемой и портом. Официальные параметры приведены в разделе CORS.

import { cors } from 'hono/cors';
app.use('/api/*', cors({
  origin: 'https://demo.example',
  allowMethods: ['GET', 'POST', 'OPTIONS'],
  allowHeaders: ['Content-Type', 'Authorization'],
  exposeHeaders: ['X-Request-Id']
}));

Опыт 8. SSE: показать прогресс до завершения ответа

Некоторые операции удобно показывать пользователю постепенно: проверка нескольких файлов, построение отчёта, последовательная обработка небольших порций. SSE позволяет передавать именованные текстовые события в одном ответе. В нашей мастерской приходят progress с номером шага и процентом, затем done. Полоса прогресса меняется при чтении фактических событий из ReadableStream, а не по независимому декоративному таймеру.

Учебная работа состоит из трёх-восьми шагов. Между ними намеренно выдерживается 280 миллисекунд. Это время не изображает реальный импорт и не является производительностью Hono. При переносе в приложение проценты нужно брать из измеримого состояния задачи: обработано строк из общего количества, подготовлено файлов из набора. Если общий объём неизвестен, честнее показать текущий этап без выдуманного точного процента.

В коде используется streamSSE, описанный в официальном Streaming Helper. Клиент мастерской читает поток вручную, чтобы видеть исходный текст событий и иметь кнопку остановки. В обычном интерфейсе также можно использовать EventSource, если его ограничения по методу и заголовкам подходят к вашей схеме авторизации.

import { streamSSE } from 'hono/streaming';
app.get('/api/progress', (c) => streamSSE(c, async (stream) => {
  for (let step = 1; step <= 5 && !stream.aborted; step++) {
    await stream.sleep(280); // Явная учебная задержка.
    if (stream.aborted) break;
    await stream.writeSSE({
      event: 'progress', id: String(step),
      data: JSON.stringify({ step, percent: step * 20 })
    });
  }
}));

Отмена, ошибки потока и буферизация прокси

Нажмите остановку до завершения восьми шагов. Мастерская отменяет чтение, закрывает reader и показывает состояние остановленного потока. Обработчик получает сигнал закрытия и больше не должен создавать новые порции. В проекте проверяется этот путь отдельно от успешного завершения. Сброс также отменяет текущий запрос, восстанавливает интерфейс и не позволяет позднему результату старого выполнения затереть новый экран.

Закрытие соединения не означает автоматическую отмену любой работы в системе. Если обработчик уже поставил задачу в очередь, фоновый worker продолжит её согласно собственной политике. Для отмены такой задачи потребуется отдельный контракт. Долгий CPU-цикл тоже не станет прерываемым лишь из-за наличия AbortController: вычисление должно проверять отмену и своевременно отдавать управление.

На VPS можно столкнуться с ситуацией, когда Node пишет события постепенно, но браузер получает всё сразу. Причиной бывает буферизация reverse proxy. В примерах архива для SSE отключена буферизация и установлен ограниченный таймаут чтения. Проверяйте поток через curl с ключом -N и через публичный домен, а не только localhost. После начала потока ошибку передают отдельным событием, а не новым HTTP-статусом.

# Linux: увидеть события по мере поступления.
curl -N 'http://127.0.0.1:8502/api/progress?steps=8'

# PowerShell: явное имя исполняемого файла.
curl.exe -N "http://127.0.0.1:8502/api/progress?steps=8"

Опыт 9. Один router: браузерный вызов и Node HTTP

Общая функция createApi создаёт приложение с маршрутами и собственными данными. Браузер вызывает app.request, а сервер передаёт app.fetch адаптеру @hono/node-server. Логика проверки бюджета, выдачи страниц и формирования ошибок не переписана второй раз для демо. Поэтому изменение правил в router можно проверить обоими способами, сохранив один источник прикладного поведения.

Однако экземпляры независимы. Созданная на странице заявка не появляется автоматически в памяти Node, и наоборот. Разделяемый исходный код не означает разделяемую память между процессами. Это полезно понять до подключения нескольких серверных worker или балансировки: массив внутри процесса не превращается в общую базу данных. В production данные должны жить в подходящем постоянном хранилище.

В девятом опыте кнопка проверяет контракт health и списка в браузере. Настоящий транспорт запускается только после скачивания архива. Затем curl обращается к loopback-порту через TCP/HTTP. Адаптер и особенности Node документированы в официальном репозитории Node Server. Сравнивайте форму ответа, а не ожидайте совпадения requestId или времени выполнения.

import { serve } from '@hono/node-server';
import { createApi } from './router.js';
const app = createApi();
serve({ fetch: app.fetch, hostname: '127.0.0.1', port: 8502 });

Запуск на Windows: PowerShell и JSON из файла

Установите Node.js 24 LTS, распакуйте архив в отдельную папку и откройте там PowerShell. Сначала проверьте node –version, затем восстановите зависимости, соберите public и выполните тесты. Команды npm.cmd обходят распространённую ситуацию, когда политика PowerShell запрещает запуск одноимённого скрипта npm.ps1. Изменять системную политику ради этого учебного проекта не требуется.

После npm.cmd start откройте указанный localhost-адрес. Для HTTP-запросов используйте отдельное окно терминала, пока сервер работает. JSON уже лежит в examples/job.json в UTF-8. Ключ –data-binary с именем файла избавляет от сложного экранирования кавычек и различий версий командной оболочки. В PowerShell явно пишем curl.exe, чтобы не попасть на старый alias другой команды.

Если порт занят, выберите другой через переменную PORT и используйте его во всех запросах. Не останавливайте неизвестный процесс только ради освобождения номера. Сервер по умолчанию привязан к 127.0.0.1, поэтому доступен с этой машины. Для завершения собственного стенда нажмите Ctrl+C в его терминале. Запуск без HTTP через file:// для мастерской не предусмотрен.

node --version
npm.cmd ci
npm.cmd run build
npm.cmd test
npm.cmd start

# В другом окне, из корня проекта:
curl.exe -i -X POST -H "Content-Type: application/json" --data-binary "@examples/job.json" "http://127.0.0.1:8502/api/jobs"

Запуск на Linux: тот же проект и реальный API

На Linux нужен поддерживаемый Node 24 LTS и npm. Способ установки зависит от дистрибутива и принятой политики сервера; проверьте фактическую версию, а не только наличие команды node. В рабочей папке выполните npm ci, сборку и тесты от обычного пользователя проекта. Глобально устанавливать Hono или esbuild не нужно: зависимости описаны в package.json и lock-файле.

Локальный запуск полезен для проверки контракта перед публикацией сервиса. Получите health, создайте запись из JSON-файла, прочитайте её по Location и откройте поток. Если эти действия работают, можно переходить к systemd и reverse proxy. Не начинайте диагностику сразу с публичного домена: иначе трудно понять, ошибка находится в приложении, маршрутизации сети или сертификате.

Собранный public содержит локальный Hono и Bootstrap. Внешний CDN для мастерской не нужен, но первая установка зависимостей требует доступа к npm registry. На сервер можно переносить воспроизводимо собранный релиз, однако node_modules между разными платформами копировать не следует. Нативные инструменты сборки имеют отдельные платформенные пакеты, поэтому для нового окружения используйте npm ci.

node --version
npm ci
npm run build
npm test
npm start

# В другом терминале:
curl -i 'http://127.0.0.1:8502/api/jobs?limit=3'
curl -i -X POST -H 'Content-Type: application/json'   --data-binary @examples/job.json http://127.0.0.1:8502/api/jobs

Linux/VPS: отдельный пользователь и systemd

Для постоянно работающего сервиса терминал с npm start неудобен: закрытие сессии, перезагрузка и случайное завершение требуют управления процессом. В архиве есть unit systemd. Он запускает Node от отдельного пользователя, задаёт рабочий каталог, адрес и порт, перезапускает процесс после сбоя. Node остаётся на loopback, а наружу смотрит выбранный reverse proxy.

Сначала создайте отдельного системного пользователя и каталог /opt/hono-demo, перенесите распакованный проект, восстановите зависимости и проверьте сборку. Полная последовательность есть в README. Не выполняйте копирование поверх существующего рабочего приложения: для нового релиза используйте отдельную папку и сохраните предыдущую версию. Unit предполагает /usr/bin/node; сравните этот путь с результатом command -v node.

В unit включены NoNewPrivileges, PrivateTmp и ограничения записи системных каталогов. Они подходят этому демо, которое хранит данные только в памяти. Если ваш сервис позднее начнёт писать файлы, добавьте конкретный разрешённый каталог и права, а не выключайте все ограничения без разбора. Перед публичным размещением настоящих данных замените учебную авторизацию и добавьте хранилище, резервные копии и прикладные права.

# После переноса, сборки и тестов проекта:
command -v node
sudo cp /opt/hono-demo/deploy/hono-demo.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now hono-demo
sudo systemctl status hono-demo --no-pager
curl -i http://127.0.0.1:8502/api/health
sudo journalctl -u hono-demo -n 80 --no-pager

Caddy: домен, HTTPS и проксирование к Node

Caddy удобно поставить перед loopback-сервером: он принимает запросы домена и передаёт их на Node. Для доступного публичного домена с корректными DNS и сетевыми условиями Caddy может управлять сертификатом автоматически. Замените api.example.com своим адресом, проверьте A и AAAA, доступность нужных портов и отсутствие конфликта с уже работающим веб-сервером.

Конфигурация ниже — отдельный блок домена. Его следует добавить к существующей конфигурации, сохранив остальные сайты. Лимит тела здесь выше прикладного, а flush_interval позволяет немедленно передавать порции потока. После изменения сначала выполните проверку конфигурации и только затем reload. Проверяйте и обычный JSON, и SSE через публичный HTTPS-адрес.

Этот блок не обеспечивает прикладную авторизацию и не делает публичный учебный токен секретным. Также он не создаёт резервное копирование базы, которой в демо пока нет. Для подробностей настройки доменов, PHP и нескольких upstream полезен отдельный материал Caddy: HTTPS, reverse proxy, PHP и Node.js. Конфигурацию под особенности вашего VPS нужно проверить отдельно.

api.example.com {
  request_body {
    max_size 8KB
  }
  reverse_proxy 127.0.0.1:8502 {
    flush_interval -1
  }
}

# Проверка и применение:
# sudo caddy validate --config /etc/caddy/Caddyfile
# sudo systemctl reload caddy

Nginx: альтернативный upstream и поток без задержки

Если инфраструктура уже использует Nginx, менять её ради Hono не требуется. API остаётся на loopback, Nginx проксирует запросы с нужного server_name. В примере передаются Host и информация о протоколе, ограничивается тело, отключается буферизация ответа и задаётся таймаут чтения. Это достаточная отправная точка для проверки обычных JSON-ответов и короткого учебного SSE.

Файл в архиве использует listen 80. Он демонстрирует upstream и сам по себе не создаёт HTTPS. Публичный TLS добавьте через принятую у вас схему сертификатов и проверьте перенаправление, срок действия и цепочку. Не ставьте две разные службы одновременно на один внешний порт без осознанной схемы. Между Caddy и Nginx в данном примере выбирают один фронтовой прокси.

Для долгих потоков таймаут нужно согласовать с heartbeat и характером приложения. Не увеличивайте его до бесконечности без необходимости: зависшие соединения тоже расходуют ресурсы. Настройки кеша и буферизации лучше применять по маршрутам реального сервиса. Дополнительные повседневные настройки разобраны в статье Nginx: reverse proxy, кеш и диагностика.

location / {
  proxy_pass http://127.0.0.1:8502;
  proxy_http_version 1.1;
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_buffering off;
  proxy_read_timeout 30s;
}
# Внутри server: client_max_body_size 8k;
# sudo nginx -t
# sudo systemctl reload nginx

Проверки: app.request, настоящий HTTP и границы результата

Тесты в архиве проверяют не только успешную карточку. Они создают заявку, читают её обратно, отклоняют неизвестные поля, различают ошибки JSON и данных, проверяют 401 и 403, границы пагинации, размер тела и общий формат исключения. Отдельные проверки читают реальные события SSE, отменяют reader и выполняют все подготовленные варианты девяти сценариев.

Транспортный тест запускает настоящий Node adapter на случайном loopback-порту. Через fetch он отправляет POST, получает список, читает поток и проверяет раздачу JavaScript с правильным MIME. После проверки собственный сервер закрывается. Это помогает обнаружить проблему, которую in-memory вызов не заметил бы: например, неправильную настройку статических файлов или отличие поведения HTTP-адаптера.

Такие проверки не заменяют приёмку публичного сайта. После загрузки архива и мастерской нужно отдельно открыть опубликованные ссылки, скачать файл, проверить интерфейс на узком экране и пройти ключевые действия клавиатурой. А фактические DNS, сертификат, firewall и systemd вашего сервера требуют проверки в вашем окружении. Приём app.request описан в руководстве Testing.

import test from 'node:test';
import assert from 'node:assert/strict';
import { createApi } from './src/router.js';
test('карточка отсутствующей заявки', async () => {
  const response = await createApi().request('/api/jobs/999');
  assert.equal(response.status, 404);
  assert.equal((await response.json()).error.code, 'JOB_NOT_FOUND');
});

Типичные проблемы и понятная диагностика

Если npm не найден, сначала проверьте установленный Node и откройте новый терминал после установки. Если PowerShell блокирует npm.ps1, используйте npm.cmd. Если сервер сообщает занятый порт, задайте другой PORT и обновите адреса в своих запросах. Эти ошибки относятся к запуску окружения, а не к обработке маршрута Hono.

Если POST возвращает 415, проверьте Content-Type. При 400 посмотрите синтаксис JSON и способ передачи файла. При 422 прочитайте fields и ограничения контракта. При 413 сравните размер в байтах и лимиты API с прокси. Если localhost работает, а домен даёт 502, проверьте активность systemd, адрес upstream и журнал reverse proxy. Разделение слоёв сокращает случайные изменения конфигурации.

Если поток приходит целиком в конце, проверьте буферизацию и используйте curl -N. Если импорт ES-модуля блокируется браузером, убедитесь, что app.js выдаётся с JavaScript MIME; архив специально собирает публичные модули с расширением .js. Если после перезапуска исчезли записи, это ожидаемое свойство учебного массива. Для сохранения данных требуется база, миграции и продуманная схема записи.

Как перенести подход в собственное приложение

Начните с одной законченной пользовательской операции. Опишите вход, успешный ответ, возможные отказы и действия интерфейса в каждом случае. Затем отделите router от хранения и бизнес-логики. Hono должен соединять HTTP с операцией, а не превращаться в огромный файл с SQL, отправкой почты, генерацией отчётов и всеми правилами продукта одновременно.

Для рабочего сервиса добавляйте постоянное хранилище, настоящую идентичность, проверку прав на каждый ресурс и транзакции там, где несколько изменений должны быть атомарными. Продумайте повторный POST: при сетевом сбое клиент может не знать, успела ли операция завершиться. Для критичных действий пригодятся ключи идемпотентности и стабильные идентификаторы. Это решения уровня приложения, а не автоматический эффект выбора framework.

Тяжёлую обработку лучше вынести из короткого HTTP-запроса. API принимает задание, возвращает идентификатор, а интерфейс следит за состоянием. SSE может быть каналом отображения этого состояния, но не заменяет очередь и хранение результатов. Нагрузку проверяйте на своих маршрутах и данных: небольшая длительность демонстрации не обещает такую же скорость сервиса с базой, интеграциями и несколькими пользователями.

Исходники, структура архива и официальные материалы

В архиве находятся исходники router и мастерской, сервер Node, lock-файл, тесты, JSON для запросов, unit systemd и примеры Caddy/Nginx. Папка public уже собрана, но её можно воспроизвести командой npm run build. Локальные CSS и JavaScript позволяют разместить мастерскую без CDN. Авторский код распространяется по MIT, тексты лицензий установленных зависимостей собраны в отдельный файл.

Файл README объясняет оба способа выполнения, отдельную память браузера и Node, команды PowerShell и Linux, порядок проверки VPS и остановку собственного процесса. Скачанный из интерфейса request-example.mjs запускается из корня распакованного проекта после npm ci. Он показывает ровно текущий запрос; полный архив нужен для его импорта createApi и зависимостей.

Для дальнейшего изучения используйте первичные источники: Hono на Node.js, маршруты, middleware, потоковые ответы, тестирование и закреплённый код версии. Сверяйте примеры новой документации с версией своего lock-файла перед переносом в рабочий сервис.