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

Fastify: API на Node.js — возможности, мастерская и 8 задач

Как связать формы, каталог, заявки и аналитику с сервером на Fastify. Объясняем возможности и применение, пробуем запросы в мастерской и запускаем настоящий API из готового проекта.

Александр Некрасов
Fastify — понятный API на Node.js: формы, каталог и график обмениваются данными с сервером

Fastify — веб-фреймворк для Node.js, на котором можно построить сервер приложения и HTTP API. Пользователь выбирает услугу, отправляет форму или меняет статус заявки; сервер принимает запрос, проверяет данные, выполняет нужное действие и возвращает результат. Один API может обслуживать сайт, административную панель, мобильное приложение и внутреннюю интеграцию.

В этой статье разберём возможности Fastify человеческим языком: где он пригодится, какую работу возьмёт на себя и что останется за разработчиком. Затем соберём учебный сервис с каталогом услуг, заявками и аналитикой. В мини-мастерской можно менять запросы и смотреть ответы, а из архива — запустить настоящий Node.js-сервер с тем же интерфейсом и проверками.

Что вы получите. Восемь практических задач, конструктор запросов, понятные ошибки, curl и fetch, схемы JSON, исходники, тесты и инструкции запуска. Публичная мастерская выполняет браузерную модель на учебных данных. Fastify исполняется в скачиваемом Node.js-проекте; модель не выдаётся за работающий Node-сервер этого сайта.

Что умеет Fastify и какую часть сервиса он закрывает

Fastify связывает HTTP-запрос с вашим кодом. Вы объявляете адрес, метод, допустимые параметры и обработчик: например, GET /api/services возвращает услуги, а POST /api/requests создаёт заявку. Фреймворк помогает организовать маршруты, проверку данных, ответы, обработку ошибок, журналы и общие действия до или после обработчика.

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

Возможность Что это означает на практике Что получает пользователь
Маршруты У каждой операции есть метод и адрес Форма, поиск и кнопки выполняют определённые действия
JSON Schema Сервер проверяет структуру, типы и ограничения Понятное объяснение, какое поле нужно исправить
Схемы ответа Публичный результат имеет заданную форму Интерфейс получает предсказуемые поля
Hooks Общие проверки выполняются на нужном этапе Единое поведение защищённых и служебных операций
Плагины Маршруты и зависимости разделены на модули Сервис можно расширять без переписывания всех экранов
Логи и тестирование Запрос можно найти и воспроизвести Ошибки легче объяснить и исправить

Фреймворк не выбирает правила вашего бизнеса. Какие статусы допустимы, кто может изменить заявку, где хранить данные и когда отправлять уведомление — решения приложения. Fastify предоставляет место, где эти решения удобно выполнять и проверять. База данных, очередь задач и система пользователей подключаются отдельно.

Документация: Первый сервер Fastify, Маршруты и параметры, Hooks: этапы обработки запроса.

Где и зачем использовать Fastify

Ситуация Что делает сервер Практическая польза
Сайт услуг и форма заявки Проверяет услугу, количество и контактные данные, создаёт запись Форма получает номер заявки и понятный результат отправки
CRM или административная панель Отдаёт списки, принимает фильтры и изменения статуса Несколько экранов работают с одними правилами и данными
Личный кабинет Проверяет пользователя и разрешённые операции Человек видит свои документы, заказы и настройки
Мобильное приложение Возвращает тот же JSON-контракт, что и веб-интерфейсу Не нужно дублировать бизнес-правила для каждой платформы
Панель аналитики Собирает агрегаты за выбранный период Графики получают готовые показатели вместо всей базы
Интеграция между сервисами Принимает webhook или запрос другой системы Действия можно связывать без ручного переноса данных

Хорошая первая задача — небольшой API с одним понятным процессом. Например, каталог → создание заявки → получение её статуса. Такой маршрут уже полезен для реального интерфейса и позволяет освоить проверку, ошибки и тесты. Не обязательно сразу строить десятки микросервисов: один аккуратно разделённый сервер часто проще сопровождать.

Для статической страницы без серверных операций Fastify может быть не нужен. Если проект уже решает задачу средствами WordPress или PHP, выбор Node.js должен давать конкретную пользу: общий JavaScript-код команды, нужную библиотеку, отдельный API или существующую Node-инфраструктуру. Скорость фреймворка сама по себе не исправляет медленный SQL-запрос и тяжёлый обработчик.

На сайте уже есть CRM «Сфера» на Vue: её формы, списки и аналитика показывают, кому нужен такой API. Puppeteer может быть отдельным исполнителем браузерной задачи, а Fastify — принимать задание и отдавать его состояние. Долгую работу при этом выносят из обработки HTTP-запроса.

Документация: Рекомендации для приложения.

Как запрос превращается в результат интерфейса

  1. 1. Действие пользователяВыбор услуги, поиск, отправка формы или смена статуса.
  2. 2. HTTP-запросМетод, адрес, заголовки и JSON с нужными значениями.
  3. 3. Fastify и ваш обработчикПроверка структуры, прав и бизнес-правил; чтение или изменение данных.
  4. 4. Ответ и новый экранСтатус HTTP и JSON: данные, подтверждение либо объяснение ошибки.

Представьте кнопку «Создать заявку». Браузер отправляет выбранную услугу и количество. Сервер проверяет формат, находит услугу, рассчитывает сумму по своим данным и создаёт запись. В ответе находятся ID, статус и итоговая сумма. Только после успешного ответа интерфейс показывает подтверждение и обновляет список.

Здесь важно разделять три проверки. Формат отвечает на вопрос «пришло ли целое положительное количество». Права — «может ли этот пользователь создавать такую заявку». Бизнес-правило — «существует ли услуга и разрешена ли операция сейчас». Валидный JSON может не пройти следующие этапы; это нормально и должно иметь понятный ответ.

Цена в учебном API берётся из каталога сервера. Клиент передаёт serviceId и quantity, а не доверенную сумму. Аналогично, в рабочем сервисе владелец записи определяется по проверенной сессии пользователя, а не по произвольному userId из формы. Такой подход помогает сохранить согласованность между экраном и сервером.

Документация: Данные запроса, Hooks: этапы обработки запроса.

Мини-мастерская: измените запрос и посмотрите, что получит интерфейс

Начните с каталога услуг: поменяйте поиск и номер страницы. Затем создайте заявку, отправьте вариант с ошибкой, прочитайте запись по ID и измените статус. Сравните успешный ответ и ошибку: HTTP-статус, JSON и сообщения для человека. Мастерская показывает, как один запрос влияет на учебный интерфейс, а рядом можно получить код для своего проекта.

Открыть мастерскую на весь экран ↗Скачать Node.js-проект ZIP

На странице работает модель с собственным набором услуг и заявок. Она помогает понять контракт и безопасно пробовать изменения, не создавая записи на сервере сайта. В скачанном проекте тот же интерфейс обращается к настоящим HTTP-маршрутам Fastify. Текущий режим указан в мастерской; результаты локальных запросов можно проверить отдельно через curl и тесты.

В учебном сервере данные хранятся в памяти. После остановки и нового запуска восстанавливается исходный набор. Это удобно для повторения урока, но не заменяет базу данных. Перед внедрением в рабочий сервис понадобится долговечное хранилище, авторизация и правила доступа к каждой операции.

Установка и запуск проекта

Пример закреплён на Fastify 5.12.8, проверенном 6 ноября 2026 года. Для скачиваемого проекта используйте Node.js 22 или новее; проверочный запуск выполнен на Node 24.19.0. Сам Fastify 5 перешёл на минимальную базу Node 20, однако для нового окружения выбирайте поддерживаемую ветку Node. Библиотека распространяется по MIT; уведомления зависимостей включены в архив.

В корне распакованного проекта

node --version
npm --version
npm ci
npm start

Откройте http://127.0.0.1:8792. Этот адрес доступен на вашем компьютере. В терминале остаётся работающий сервер; остановка — Ctrl+C. Если команда выполняется из другой папки, npm может не найти package.json: сначала перейдите в каталог проекта, содержащий этот файл.

Проверки и реальные примеры HTTP

npm test
npm run examples

npm ci использует сохранённый lock-файл и устанавливает согласованный набор зависимостей. Команда npm run examples выполняет сценарии и сохраняет проверочные ответы; README объясняет расположение файлов и ожидаемые статусы. Для локального прохождения не нужны платные сервисы, внешний аккаунт или база данных.

Документация: Первый сервер Fastify, Особенности Fastify 5.

Первый сервер: маршрут, ответ и запуск

Для знакомства полезно увидеть минимальный сервер отдельно от мастерской. Создайте новую папку, выполните npm init -y и npm install fastify@5.12.8, затем сохраните файл first.mjs. Расширение .mjs позволяет использовать import; в проекте с обычными .js для ESM указывают "type": "module" в package.json.

Полный файл first.mjs

import Fastify from 'fastify';

const app = Fastify({ logger: true });
app.get('/hello', async () => ({ message: 'Сервер работает' }));

try {
  await app.listen({ host: '127.0.0.1', port: 8793 });
} catch (error) {
  app.log.error(error);
  process.exitCode = 1;
}

Запуск отдельного первого примера

node first.mjs
# В другом терминале:
curl http://127.0.0.1:8793/hello

Обработчик возвращает объект, и Fastify формирует JSON-ответ. Не нужно вручную сериализовать объект в строку и устанавливать все служебные заголовки. В реальном приложении сюда добавятся схема, сервисный метод и обработка ожидаемых ошибок. Номер 8793 выбран отдельно, чтобы пример не конфликтовал с мастерской на 8792.

В асинхронном обработчике выбирайте понятный способ завершить ответ: вернуть значение или вернуть reply.send(...). Не отправляйте один ответ, а затем продолжайте ветку, которая отправляет второй. После ранней ошибки используйте return; иначе действие может выполниться несмотря на уже отправленное сообщение.

Документация: Первый сервер Fastify, Формирование ответа.

Какие маршруты есть в учебном API

Метод и адрес Назначение Основной результат
GET /api/services Поиск и пагинация каталога 200: услуги и метаданные страницы
POST /api/requests Создание заявки 201: созданная запись
POST /api/requests с неверными полями JSON Проверка ограничений 400: поля и причины ошибок
GET /api/requests/:id Карточка заявки 200 либо 404
PATCH /api/requests/:id Изменение статуса с версией 200 либо конфликт 409
GET неизвестного адреса Единая ошибка отсутствия маршрута 404 в общем формате
GET /api/stats Данные для аналитики 200: агрегаты по заявкам
GET /api/internal/summary Учебная проверка заголовка доступа 401 без ключа, 200 с demo-ключом

Путь — часть публичного договора между сервером и клиентом. Клиенту не нужно знать, в каком файле лежит обработчик и какая библиотека хранит данные. Он знает метод, параметры и форму результата. Если эти условия меняются, обновлять нужно и клиент, и проверки, и документацию.

В проекте также есть список заявок GET /api/requests, который помогает интерфейсу обновляться после создания и изменения записи. Все данные синтетические: это учебные организации, услуги и заявки. Общий формат успешного ответа — ok, data и requestId; у списка каталога дополнительно есть meta.

Документация: Маршруты и параметры.

Параметры адреса, query, JSON и заголовки: что куда передавать

В API есть несколько способов передать данные, и каждый решает свою задачу. ID конкретной заявки удобно разместить в пути. Поиск, сортировку и страницу — в query. Поля создаваемой записи — в JSON-теле. Служебные сведения, например токен доступа или тип содержимого, — в заголовках. Не стоит помещать пароль в URL: адрес может попасть в журналы и историю.

Небольшой самостоятельный пример чтения параметров

app.get('/requests/:id', async (request) => {
  const id = request.params.id;
  const include = request.query.include;
  return { id, include: include ?? null };
});

Это поясняющий фрагмент, а не готовый обработчик доступа к заявке: в рабочем маршруте сначала нужны схемы и права. Значение request.body появляется после разбора тела запроса. В раннем onRequest на него полагаться нельзя; этапы обработки и ограничения данных должны соответствовать выбранному hook.

У HTTP-запроса также есть метод. Обычно GET читает ресурс, POST создаёт или запускает действие, PATCH изменяет часть записи. Значение этих операций определяет ваш контракт. Не стоит менять данные при обычном GET: браузер, кеш или проверяющая система могут повторить такой запрос без намерения выполнить изменение.

Документация: Данные запроса, Hooks: этапы обработки запроса, Маршруты и параметры.

Задача 1. Каталог услуг: поиск и пагинация

Каталог нужен не только интернет-магазину. Это может быть перечень услуг, тарифов, оборудования или шаблонов документов. Сервер получает поисковую строку и страницу, отбирает записи и отдаёт ограниченную порцию. Интерфейс показывает карточки и знает, есть ли следующая страница.

Поиск из Node.js или приложения

const url = new URL('/api/services', 'http://127.0.0.1:8792');
url.searchParams.set('q', 'аудит');
url.searchParams.set('page', '1');
url.searchParams.set('limit', '4');

const response = await fetch(url);
const result = await response.json();
if (!response.ok) throw new Error(result.error?.message ?? 'Ошибка каталога');
console.log(result.data, result.meta);

В мастерской попробуйте пустой поиск, слово из названия и запрос, по которому ничего нет. Пустая выдача — нормальный успешный результат: HTTP 200, пустой массив и total: 0. Это отличается от недоступного сервера или неправильного параметра. Экрану нужны разные состояния: «ничего не найдено» и «не удалось загрузить».

Ограничивайте размер страницы на сервере, даже если в интерфейсе всего три варианта. Клиент может отправить запрос самостоятельно. При подключении БД фильтрация и пагинация должны выполняться там, где хранятся данные; выгружать всю таблицу в Node для каждого поиска обычно не требуется. Для стабильного обхода добавьте определённый порядок сортировки.

Документация: Маршруты и параметры, Проверка входа и сериализация ответа.

Задача 2. Создание заявки и ответ 201

Форма передаёт минимальные сведения: заказчика, услугу, количество и примечание. Сервер проверяет их, получает цену из своего каталога и рассчитывает итог. Результат должен позволить интерфейсу сразу показать карточку созданной заявки, без угадывания её номера или повторного чтения всего списка.

Тело POST /api/requests

{
  "customer": "Команда Кедр",
  "serviceId": "SVC-01",
  "quantity": 1,
  "note": "Учебная заявка"
}

Создание заявки настоящим запросом

const response = await fetch('http://127.0.0.1:8792/api/requests', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    customer: 'Команда Кедр', serviceId: 'SVC-01',
    quantity: 1, note: 'Учебная заявка'
  })
});
const result = await response.json();
if (response.status !== 201) throw new Error(result.error?.message ?? 'Заявка не создана');
console.log(result.data.id, result.data.status);

Ответ 201 означает создание ресурса; в учебном приложении новая заявка получает статус new и версию 1. Сумма возвращается как amountMinor в копейках вместе с currency: "RUB". Это явное соглашение: интерфейс форматирует рубли для показа, а расчёты не зависят от текста «1 200 ₽» в HTML.

Создание заявки из Windows PowerShell

$body = @{
  customer = 'Команда Кедр'
  serviceId = 'SVC-01'
  quantity = 1
  note = 'Запрос из PowerShell'
} | ConvertTo-Json

$result = Invoke-RestMethod -Method Post `
  -Uri 'http://127.0.0.1:8792/api/requests' `
  -ContentType 'application/json; charset=utf-8' `
  -Body ([System.Text.Encoding]::UTF8.GetBytes($body))
$result.data

Команды npm выше одинаковы для основных терминалов. В примерах curl используйте настоящий curl; в Windows PowerShell его явно вызывают как curl.exe. Многострочные команды Bash из мастерской используют обратную косую черту; в PowerShell перенос оформляется обратным апострофом, как в примере выше.

Свойство note полезно для демонстрации, но реальные примечания могут содержать личную информацию. Не записывайте полный JSON формы в журнал по умолчанию. В рабочем сервисе отдельно определите обязательные поля, правила хранения, доступ сотрудников и срок сохранения данных.

Документация: Формирование ответа, Маршруты и параметры.

Задача 3. Проверка JSON и ошибки конкретных полей

JSON Schema описывает ожидаемую форму: объект или массив, обязательные поля, строки, числа и допустимые значения. Это позволяет отклонить ошибочный запрос до выполнения операции. Для клиента полезна не просто фраза «400 Bad Request», а связь ошибки с конкретным полем.

Пример полной схемы тела заявки

const bodySchema = {
  type: 'object',
  additionalProperties: false,
  required: ['customer', 'serviceId', 'quantity'],
  properties: {
    customer: { type: 'string', minLength: 2, maxLength: 80 },
    serviceId: { type: 'string', pattern: '^SVC-[0-9]{2}$' },
    quantity: { type: 'integer', minimum: 1, maximum: 20 },
    note: { type: 'string', maxLength: 300, default: '' }
  }
};

В Fastify 5 схема объекта должна явно содержать type: "object" и properties. Схемы определяет разработчик: их нельзя принимать от произвольного посетителя и компилировать как доверенный код. Проверку существования услуги или обращение к БД выполняют после структурной валидации, в подходящем hook или сервисном слое.

Обратите внимание на настройки валидатора. В учебном проекте включены приведение типов и значения по умолчанию, а удаление лишних свойств отключено. Поэтому строковый параметр страницы может стать числом, но неизвестное поле в объекте с additionalProperties: false вызовет ошибку. Это осознанные настройки проекта: поведение нельзя выводить только из вида схемы без учёта Ajv.

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

Документация: Проверка входа и сериализация ответа, Особенности Fastify 5.

Задача 4. Карточка заявки по ID

После списка человек открывает одну запись. API должен вернуть нужную карточку или определённо сообщить, что её нет. В учебном наборе можно начать с RQ-1001. Полученная версия записи понадобится в следующей задаче для изменения статуса.

Получение существующей заявки

curl http://127.0.0.1:8792/api/requests/RQ-1001

Чтение по ID само по себе не означает права доступа. В настоящей CRM сначала определяют пользователя и организацию, затем проверяют, относится ли заявка к ним. Знание чужого ID не должно давать доступ к записи. Учебный API использует только вымышленные общие данные и не моделирует полноценную систему пользователей.

Если ID синтаксически неверен, ответ может быть 400; если формат допустим, но запись отсутствует — 404. Это разные причины и разные подсказки для разработчика. Интерфейсу полезно уметь обработать исчезнувшую карточку: например, пользователь открыл старую ссылку или запись была удалена другим сотрудником.

Документация: Маршруты и параметры, Данные запроса.

Задача 5. Изменение статуса и конфликт версий

Два человека могут открыть одну заявку одновременно. Если каждый сохранит своё состояние без проверки, более поздний запрос затрёт промежуточное изменение. Учебный проект показывает простой вариант оптимистической проверки: клиент отправляет версию, которую он получил при чтении.

Тело PATCH /api/requests/RQ-1001

{
  "status": "in_progress",
  "version": 1
}

Если версия совпала, сервер меняет статус, увеличивает версию и возвращает новую запись. Повторите тот же запрос с версией 1: после первого успешного изменения он должен получить конфликт 409. Интерфейс перечитывает карточку, показывает актуальные данные и предлагает осознанно повторить изменение.

В примере действует последовательность new → in_progress → done. Новую заявку и заявку в работе также можно отменить: cancelled. Завершённые и отменённые записи не открываются повторно. Недопустимый переход возвращает 409 с кодом STATUS_CONFLICT, даже если сама строка статуса прошла схему. В вашем сервисе правила могут отличаться: их определяет бизнес-логика отдельно от проверки формата.

При переносе в БД сравнение версии и запись должны быть одной атомарной операцией, например обновлением с условием по текущей версии. Последовательность «прочитать → сравнить → записать» без защиты между несколькими процессами оставляет гонку. Учебное хранение в памяти объясняет контракт, но не заменяет транзакционный механизм.

Документация: Маршруты и параметры, Подключение базы данных.

Задача 6. Единый формат ошибок и неизвестный маршрут

Клиенту проще работать, когда ошибки разных операций имеют общую оболочку. Статус HTTP сообщает класс проблемы, машинный код помогает выбрать действие, сообщение объясняет ситуацию человеку, а requestId связывает её с журналом. Нельзя возвращать 200 для любой ошибки и заставлять каждый клиент угадывать результат.

Иллюстрация общей оболочки ошибки

{
  "ok": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Запись не найдена"
  },
  "requestId": "идентификатор-конкретного-запроса"
}
Статус Значение в нашем API Как реагировать интерфейсу
400 Неверные параметры, JSON или поля Сохранить ввод и показать, что исправить
401 Учебный ключ отсутствует или неверен Объяснить необходимость подходящих учётных данных
404 Адрес или ресурс не найден Предложить вернуться к списку или проверить адрес
409 Конфликт версии, перехода статуса или доступности услуги Прочитать код ошибки: обновить карточку или выбрать допустимую операцию
500 Непредвиденная ошибка сервера Показать общий текст и ID для обращения

В Fastify для ошибок обработчика применяют setErrorHandler, а для отсутствующего маршрута — setNotFoundHandler. Не раскрывайте клиенту stack trace, строки подключения и внутренние пути. Подробность нужна в защищённом журнале, а в ответе — стабильный публичный код и объяснение, позволяющее продолжить работу.

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

Документация: Ошибки Fastify, Параметры сервера и завершение работы, Формирование ответа.

Задача 7. Агрегаты для графика и панели показателей

Графику обычно нужны суммы и количества по группам, а не полная таблица записей. Маршрут GET /api/stats возвращает общее число заявок, сумму в копейках, число услуг и распределение по статусам. Так сервер хранит правило расчёта в одном месте, а разные экраны используют одинаковые показатели.

Подготовка данных для графика

const response = await fetch('http://127.0.0.1:8792/api/stats');
const result = await response.json();
if (!response.ok) throw new Error(result.error?.message ?? 'Нет статистики');
const labels = result.data.byStatus.map(item => item.status);
const counts = result.data.byStatus.map(item => item.count);
console.log({ labels, counts });

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

Для аналитики отдельно определяют период, часовой пояс и смысл показателя. «Сумма заявок» не обязательно означает полученные деньги, а статус done не всегда равнозначен оплате. Подписи должны соответствовать вычислению. Подробный разбор визуализации есть в статье Chart.js.

Документация: Маршруты и параметры, Формирование ответа.

Задача 8. Общая проверка перед защищённым маршрутом

Hooks позволяют выполнить код на определённом этапе запроса. Например, проверить учётные данные до обращения к служебному обработчику. В мастерской /api/internal/summary требует заголовок x-demo-key: demo-local-key. Сначала отправьте запрос без него, затем добавьте учебное значение и сравните ответы.

Упрощённая иллюстрация проверки в preHandler

async function demoGuard(request, reply) {
  if (request.headers['x-demo-key'] !== 'demo-local-key') {
    return reply.code(401).send({
      ok: false,
      error: { code: 'UNAUTHORIZED', message: 'Нужен учебный ключ' },
      requestId: request.id
    });
  }
}

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

Валидация формы и авторизация решают разные задачи. Даже идеально соответствующий схеме запрос может быть запрещён пользователю. В рабочем API обычно различают 401 — не подтверждённую личность — и 403 — отсутствие права при известном пользователе. CORS также не заменяет эти проверки: это браузерное правило доступа к ответам, а не защита от всех HTTP-клиентов.

Документация: Hooks: этапы обработки запроса, Области действия плагинов.

Схема ответа: понятный контракт и лишние поля

У базы часто больше полей, чем следует отдавать клиенту. Схема ответа задаёт публичную форму и помогает сериализовать результат. Например, внутренняя запись может содержать служебные отметки, но экрану нужны только ID и статус. Это полезно и для стабильности клиента, и для уменьшения случайного раскрытия лишних полей.

Самостоятельный пример публичного DTO

app.get('/public-summary', {
  schema: {
    response: {
      200: {
        type: 'object',
        required: ['id', 'status'],
        additionalProperties: false,
        properties: {
          id: { type: 'string' },
          status: { type: 'string' }
        }
      }
    }
  }
}, async () => ({ id: 'RQ-1001', status: 'new', internalNote: 'Не для ответа' }));

Это дополнительный поясняющий маршрут, которого нет среди восьми пресетов. При указанной схеме в публичный JSON попадают заданные свойства. Однако схема ответа не проверяет, имеет ли человек право видеть запись. Сначала нужно выбрать разрешённые данные, затем сформировать публичный объект и сериализовать его. Схемы разных HTTP-статусов также должны соответствовать реальным ответам.

Документация: Проверка входа и сериализация ответа, Формирование ответа.

Плагины и области действия: как не собрать всё в одном файле

По мере роста сервиса маршруты удобно группировать по предметной области: каталог, заявки, служебные операции. Плагин Fastify — функция, которая регистрирует маршруты, hooks и зависимости в своём контексте. Префикс помогает разместить группу под общим путём, например /api.

Самостоятельная иллюстрация модуля маршрутов

async function catalogPlugin(app) {
  app.get('/services', async () => ({ ok: true, data: [] }));
}

await app.register(catalogPlugin, { prefix: '/api' });
// Получившийся адрес: GET /api/services

По умолчанию регистрация создаёт дочернюю область действия: настройки и зависимости родителя доступны потомкам, но соседний плагин не должен случайно получить внутренний hook другого модуля. Это помогает ограничить служебную проверку конкретной группой адресов. Порядок регистрации зависимостей имеет значение: модуль использует уже подготовленные ему возможности.

Не оборачивайте каждый файл в fastify-plugin автоматически. Этот инструмент нужен, когда вы осознанно меняете поведение инкапсуляции, например предоставляете общую зависимость. У него есть нюансы взаимодействия с префиксом маршрутов. Для обычной группы адресов сначала достаточно стандартного register.

Документация: Регистрация плагинов, Области действия плагинов.

Как подключить форму, таблицу или Vue-приложение

Интерфейсу не обязательно знать, что сервер написан на Fastify. Он использует HTTP. HTML-форма с JavaScript, Vue-компонент, таблица Tabulator и мобильный клиент могут обращаться к одному контракту. Важно одинаково трактовать успешный ответ и ошибку.

Обёртка для JSON API на том же адресе сайта

async function api(path, options = {}) {
  const response = await fetch(path, options);
  const result = await response.json();
  if (!response.ok) {
    const error = new Error(result.error?.message ?? `HTTP ${response.status}`);
    error.details = result.error?.details ?? [];
    error.requestId = result.requestId;
    throw error;
  }
  return result.data;
}

const request = await api('/api/requests/RQ-1001');
console.log(request.status);

Этот компактный пример предполагает, что сервер отвечает JSON. В рабочем клиенте дополнительно обрабатывают недоступную сеть, таймаут через AbortController и неожиданный HTML от proxy. Не выводите сырые ответы через innerHTML; для обычного текста используйте безопасное текстовое представление.

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

Документация: Формирование ответа, Данные запроса.

Тесты: проверить API без порта и через настоящий HTTP

fastify.inject() позволяет передать запрос приложению и проверить ответ без открытия сетевого порта. Это удобно для контрактов: проверить валидный и неверный ввод, 404, конфликт версии и форму JSON. Тесты должны подтверждать важное поведение, а не просто повторять строки реализации.

Тест из корня скачанного проекта

import test from 'node:test';
import assert from 'node:assert/strict';
import { buildApp } from './src/app.mjs';

test('каталог возвращает ограниченную страницу', async () => {
  const app = await buildApp({ logger: false });
  try {
    const response = await app.inject({
      method: 'GET', url: '/api/services?page=1&limit=4'
    });
    assert.equal(response.statusCode, 200);
    assert.equal(response.json().data.length, 4);
    assert.equal(response.json().meta.limit, 4);
  } finally {
    await app.close();
  }
});

Для изоляции каждый тест получает новое приложение и собственный набор данных. Отдельно полезен реальный HTTP-прогон: он проверяет запуск сервера, разбор заголовков и запросы клиента по сети. В архиве есть оба уровня. Успех inject не означает автоматически, что правильно настроены DNS, reverse proxy и HTTPS на будущем хостинге.

Документация: Тестирование через inject и HTTP.

Логи и requestId: как найти конкретную ошибку

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

Самостоятельный пример настройки журнала

const app = Fastify({
  logger: {
    level: 'info',
    redact: ['req.headers.authorization', 'req.headers.cookie',
      'req.headers["x-demo-key"]']
  },
  requestIdHeader: false
});

app.get('/check', async (request) => {
  request.log.info({ operation: 'check' }, 'Запрос обработан');
  return { ok: true, requestId: request.id };
});

Fastify использует Pino для структурированных журналов. Редактирование перечисленных полей помогает убрать заголовки, если они входят в записываемый объект; оно не делает любой произвольный лог безопасным. Не передавайте в журнал полное тело формы или строку, в которую уже подставлен секрет. Список скрываемых путей нужно сверять с реальной структурой сообщений.

В учебном проекте request ID создаёт сервер. Если в инфраструктуре принимают ID от доверенного proxy, отдельно определяют границу доверия и формат. Идентификатор запроса помогает диагностике, но не подтверждает личность пользователя и не служит токеном доступа.

Документация: Журналы и идентификаторы запросов, Параметры сервера и завершение работы.

Память, база данных и бизнес-логика

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

При переходе к БД выделите операции чтения и записи в отдельный слой. Обработчик занимается HTTP-контрактом, сервис — правилами операции, репозиторий — запросами к хранилищу. Для нескольких связанных изменений используйте подходящую транзакцию. Схема JSON отвечает за форму входа, а ограничения БД и бизнес-правила дополняют её.

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

Сравните этот подход с Symfony Validator в PHP: язык и библиотека отличаются, а разделение формата, прав, правил и сохранения остаётся полезным. Выбирайте инструменты под существующий проект и возможности команды.

Документация: Подключение базы данных, Рекомендации для приложения.

Запуск, корректная остановка и публикация сервера

На компьютере разработчика сервер слушает loopback-адрес 127.0.0.1. Для размещения в контейнере или на хостинге адрес, порт и сетевой доступ задают согласно инфраструктуре. Не стоит просто заменить адрес и считать приложение подготовленным к публичному доступу: нужны HTTPS, правила доступа, ограничения запросов, хранение данных и журналы.

Корректное завершение вместо немедленного process.exit

let closing = false;
async function shutdown() {
  if (closing) return;
  closing = true;
  try {
    await app.close();
  } catch (error) {
    app.log.error(error);
    process.exitCode = 1;
  }
}
process.once('SIGINT', shutdown);
process.once('SIGTERM', shutdown);

app.close() запускает предусмотренный Fastify процесс остановки и hooks закрытия. Подключения к БД и другие ресурсы нужно освобождать в подходящих hooks. Не оставляйте необработанные фоновые таймеры и бесконечные операции. Внешний менеджер процессов тоже должен иметь разумный срок ожидания завершения.

Тяжёлое вычисление, обработка большого архива или длинный сценарий Puppeteer может занять обработчик надолго. Для таких задач обычно принимают задание, возвращают его ID и выполняют работу отдельно. async сам по себе не переносит вычисления в другой поток: синхронная тяжёлая работа всё ещё блокирует цикл событий.

За reverse proxy настройку trustProxy включают осознанно: приложение должно понимать, каким посредникам можно доверять сведения о клиентском адресе и протоколе. При разных origin интерфейса и API настройте CORS под нужные адреса. Конкретные конфигурации Caddy и Nginx стоит рассматривать вместе с архитектурой развёртывания.

Документация: Параметры сервера и завершение работы, Рекомендации для приложения.

Fastify, Express, обычный Node.js и PHP: как выбирать

Вариант Когда удобен Что учитывать
Fastify Нужен Node.js API с явными схемами, плагинами и тестируемыми маршрутами Бизнес-правила, БД и авторизацию проектирует приложение
Express Команда уже использует его подход и экосистему middleware Проверку данных и форму контрактов собирают выбранными компонентами
Node.js http Нужен небольшой низкоуровневый HTTP-обработчик Больше служебной работы придётся написать самостоятельно
PHP-приложение или WordPress Задача естественно продолжает существующий PHP-проект Отдельный Node-сервис добавит эксплуатационные обязанности

Fastify и HTTP-клиент решают разные стороны обмена. Fastify принимает запросы к вашему API. Встроенный fetch в Node или Guzzle в PHP отправляют запросы другому серверу. В одной системе они могут использоваться вместе: например, PHP-приложение обращается к отдельному Fastify-сервису.

Не переносите заявленные benchmark-цифры напрямую на свой проект. Время ответа зависит от БД, внешних сервисов, объёма данных и вашей логики. Начните с понятного контракта и измерьте реальную нагрузку. Для небольшого сервиса удобство сопровождения и предсказуемые ошибки часто важнее выбора по одной синтетической таблице производительности.

Документация: Рекомендации для приложения.

Частые вопросы и ошибки при первом API

Вопрос или симптом Что проверить
Почему сервер не видит JSON? Content-Type: application/json, корректность JSON и ограничение размера тела
Почему число пришло строкой или изменило тип? Query передаётся текстом; посмотрите настройки coercion валидатора
Почему неизвестное поле исчезло или вызвало ошибку? additionalProperties и removeAdditional задают совместное поведение
Почему после перезапуска нет созданной заявки? В учебном проекте хранилище в памяти; рабочему сервису нужна БД
Почему повтор PATCH дал 409? Клиент отправил старую версию, сначала перечитайте запись
Почему fetch не бросил исключение при 404? fetch требует явной проверки response.ok или status
Можно ли поставить Fastify обычным script на страницу? Сервер Fastify исполняется в Node.js; браузер отправляет ему HTTP-запросы
Можно ли доверять валидации формы в браузере? Сервер повторно проверяет запрос независимо от интерфейса

Если результат непонятен, сократите задачу до одного запроса. Запишите метод, адрес, заголовки, тело, ожидаемый статус и фактический ответ. Затем проверьте этот маршрут через inject и реальный HTTP. Такой маленький воспроизводимый пример полезнее предположения, что «не работает весь Fastify».

Документация: Ошибки Fastify, Проверка входа и сериализация ответа, Тестирование через inject и HTTP.

Исходники и порядок дальнейшей работы

Начните с каталога, затем создайте заявку и намеренно отправьте неверные поля. Прочитайте карточку, измените статус и повторите запрос со старой версией. После этого сравните статистику, попробуйте служебный маршрут без ключа и с ним, изучите тесты и журнал. Получится связанный маршрут от действия пользователя до состояния сервера.

Скачать исходники и локальный серверОткрыть мини-мастерскую ↗

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

Официальная документация размещена рядом с соответствующими темами. Основные точки входа: Fastify, репозиторий проекта и пакет npm. Версии в архиве закреплены, а перед обновлением зависимостей стоит повторить тесты и проверку настоящих запросов.