Бэкенд на Fastify
До этого момента в нашей схеме был только фронтенд на React. В рамках одной из прошлых лабораторных мы для него подготовили моки данных, в другой — собрали Docker-окружение. Но реального бэкенда в схеме до сих пор не было. Настало время это исправить.
В этой лабораторной мы:
- инициализируем Node.js-проект с Fastify v5;
- подключим инфраструктурные плагины (security headers, CORS, rate-limit, OpenAPI/Swagger);
- опишем типы и схемы валидации через TypeBox;
- развернём
app.tsс обработчиками ошибок и парой реальных маршрутов; - настроим
server.tsс graceful shutdown; - свяжем backend с фронтендом через Vite-прокси и научимся переключаться между MSW и реальным API.
База данных, миграции, Docker-сборка backend и общий compose — это уже задача следующей лабораторной. Здесь данные хранятся прямо в памяти процесса, чтобы сосредоточиться на самом фреймворке.
Перенос фронтенда в frontend/
До этого момента весь наш проект жил в корне (room-assets/). Сейчас, когда у нас появится ещё и backend, удобно положить фронтенд и бэкенд в соседние папки. Сделаем простой ручной переезд:
# В корне проекта (room-assets/)
mkdir frontend
git mv src public index.html package.json package-lock.json \
tsconfig*.json vite.config.ts .gitignore eslint.config.* \
Dockerfile .dockerignore nginx \
frontend/ 2>/dev/null || true
# Старый docker-compose.dev.yml из лабы про Docker удаляем —
# в следующей лабе он будет заменён на compose.base.yaml + compose.dev.yaml
rm -f docker-compose.dev.yml docker-compose.ymlnginx/ или Dockerfile ещё не созданы) — git mv пропустит недостающее. Главное, чтобы внутри frontend/ оказались все исходники Vite-проекта. Если проект пока не под git — используйте обычный mv.После этого в корне останется только общая инфраструктура (она появится в следующей лабораторной), а собственно код фронтенда будет лежать внутри frontend/.
Настройка среды
В качестве основного фреймворка будем использовать Fastify v5 — быстрый backend-фреймворк на Node.js. Создадим рядом с frontend/ папку backend и проинициализируем там проект:
mkdir backend && cd backend
npm init -y
# Fastify v5 + плагины
npm i fastify @fastify/swagger @fastify/helmet @fastify/rate-limit @fastify/cors @sinclair/typebox @fastify/type-provider-typebox
# Типы и TS-инфраструктура (tsx используем для dev-запуска ESM+TS без бубнов)
npm i -D typescript tsx @types/node
npx tsc --init --rootDir src --outDir dist --esModuleInteropПо умолчанию tsc --init сгенерирует tsconfig.json с "module": "commonjs". У нас же package.json будет помечен "type": "module", а в коде мы будем писать ESM-импорты вида import './app.js'. Чтобы и tsc, и tsx правильно их разрешали, в сгенерированном tsconfig.json подкрутим несколько опций (остальное, что добавил tsc --init, оставляем как есть):
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"rootDir": "src",
"outDir": "dist",
"strict": true,
"skipLibCheck": true
}
}"module": "NodeNext" TypeScript обязывает указывать расширение .js в относительных импортах исходных .ts-файлов (import './app.js') — это не опечатка и не баг, так работает Node-ESM. После сборки tsc это превратится в честный ./app.js, а в dev tsx сам резолвит такой импорт в соседний app.ts.Кроме самого Fastify мы устанавливаем смежные плагины, которые будут использоваться в проекте:
- swagger (генерация OpenAPI) — в рамках лабораторной с моками мы видели пример файла OpenAPI 3.1, к которому мы движемся. В следующей лабораторной к этой спецификации мы подключим Scalar — современный UI для интерактивного просмотра.
- helmet — установка безопасных заголовков (headers) для ответа.
- rate-limit — ограничение количества запросов к API и настройка политик безопасности с целью предотвращения спама запросами (DDoS-атаки).
- cors — это механизм, который контролирует доступ к ресурсам веб-страницы по сети. Подробное и доступное описание можно посмотреть в этой статье на Хабре. Советую посмотреть, написано достаточно доступно и наглядно.
- @sinclair/typebox + @fastify/type-provider-typebox — описание схем валидации запросов/ответов на TypeScript, с возможностью автоматически выводить типы.
Теперь настроим базовые скрипты в уже знакомом файле package.json:
{
...
"type": "module",
"scripts": {
"dev": "tsx src/server.ts",
"build": "tsc",
"start": "node dist/server.js"
}
...
}tsx использовать связку node --loader ts-node/esm src/server.ts, но в Node 22+ флаг --loader помечен как deprecated и выводит warning при каждом старте. tsx решает ровно ту же задачу — на лету компилирует TS и разруливает Node-ESM с расширениями .js в импортах — и при этом не требует ручной возни с --import/register.Fastify-сервер
Теперь создадим базовый каркас сервера. Мы намеренно делим код на несколько модулей — даже если сейчас каждый из них короткий, это сразу задаёт правильную структуру для дальнейшего роста backend’а.
import type { FastifyError, FastifySchemaValidationError } from 'fastify'
import type { SchemaErrorDataVar } from 'fastify/types/schema.js'
import { Type as T, type Static } from '@sinclair/typebox'
// Этот модуль собирает переиспользуемые типы и схемы, которые нужны маршрутам Fastify и плагинам.
/**
* Обёртка над стандартной ошибкой Fastify для случаев, когда схема запроса не проходит валидацию.
* Мы расширяем Error, чтобы получить сообщение и stack trace, и одновременно реализуем FastifyError,
* чтобы Fastify понимал код ошибки и корректно возвращал ответ клиенту.
*/
export class ValidationProblem extends Error implements FastifyError {
public readonly name = 'ValidationError'
public readonly code = 'FST_ERR_VALIDATION'
public readonly statusCode = 400
public readonly validation: FastifySchemaValidationError[]
public readonly validationContext: SchemaErrorDataVar
constructor(
message: string,
errs: FastifySchemaValidationError[],
ctx: SchemaErrorDataVar,
options?: ErrorOptions
) {
super(message, options)
this.validation = errs
this.validationContext = ctx
}
}
// Схема ответа в формате RFC 9457 / Problem Details — единый JSON-формат для сообщений об ошибках (ранее RFC 7807, заменён на 9457).
export const ProblemDetails = T.Object(
{
type: T.String({ description: 'URI с подробным описанием ошибки (по умолчанию about:blank)' }),
title: T.String({ description: 'Короткое человекочитаемое резюме проблемы' }),
status: T.Integer({ minimum: 100, maximum: 599, description: 'HTTP-статус, с которым был отправлен ответ' }),
detail: T.Optional(T.String({ description: 'Дополнительные сведения о том, что пошло не так' })),
instance: T.Optional(T.String({ description: 'URI запроса, в котором возникла проблема' })),
errorsText: T.Optional(T.String({ description: 'Сводное описание всех ошибок, собранных валидацией Fastify' })),
},
{ additionalProperties: true }
)
export type ProblemDetails = Static<typeof ProblemDetails>
// Схема и тип пользователя, которые используются и в валидаторах, и в ответах API.
export const User = T.Object({
id: T.String({ description: 'Уникальный идентификатор пользователя' }),
email: T.String({ format: 'email', description: 'Адрес электронной почты' }),
})
export type User = Static<typeof User>
// Минимальная схема для health-check запроса.
export const Health = T.Object({
ok: T.Boolean({ description: 'Флаг готовности сервиса' }),
})
export type Health = Static<typeof Health>openapi.json) будет генерироваться автоматически на основании схем и комментариев к маршрутам в коде. За это отвечает модуль Swagger. UI для просмотра спецификации (Scalar) подключим в следующей лабораторной — там же, где будем разворачивать backend в Compose.Запуск и связка с фронтендом
Шаг 1 — Поднять backend локально
Из папки backend/ выполните:
npm run devВ консоли вы увидите примерно следующее:
{"level":30,"msg":"Server listening at http://0.0.0.0:3000"}Откройте в браузере:
- http://localhost:3000/api/health — должен вернуть
{"ok":true}; - http://localhost:3000/api/users — список из двух in-memory пользователей;
- http://localhost:3000/openapi.json — сгенерированная OpenAPI-спецификация (тот самый JSON, который мы видели в моках).
Шаг 2 — Настроить Vite-прокси на фронтенде
Чтобы фронтенд (на :5173) обращался к backend (на :3000), настроим прокси в Vite: тогда /api/* запросы из браузера будут уходить на backend, как если бы они шли на тот же домен. В следующей лабораторной эту функцию возьмёт на себя NGINX.
Дополним frontend/vite.config.ts секцией server.proxy:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
server: {
host: true,
port: 5173,
strictPort: true,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
},
})Шаг 3 — Запустить фронт и бэк параллельно
В двух терминалах:
cd frontend && npm run devcd backend && npm run devОткройте http://localhost:5173 — фронтенд работает как обычно, но запросы к /api/users уже идут на реальный Fastify-сервер.
Переключение между моками и реальным API
В лабораторной про моки мы регистрировали MSW прямо в main.tsx:
async function enableMocking() {
if (import.meta.env.DEV) {
const { worker } = await import('./mocks/browser')
await worker.start({ onUnhandledRequest: 'bypass' });
}
}Пока MSW активен в dev-режиме, он перехватывает запросы на уровне Service Worker — до того, как они уйдут в Vite-прокси. Это значит, что /api/users сейчас всё ещё может ловиться MSW, а не уходить на наш Fastify.
Перепишем условие через переменную окружения, чтобы можно было осознанно переключаться:
async function enableMocking() {
if (import.meta.env.DEV && import.meta.env.VITE_USE_MOCKS === 'true') {
const { worker } = await import('./mocks/browser')
await worker.start({
onUnhandledRequest: 'bypass',
serviceWorker: { url: '/mockServiceWorker.js' },
});
}
}И заведём два варианта .env:
VITE_USE_MOCKS=trueVITE_USE_MOCKS=false.env.local Vite читает поверх .env.development и не коммитит его в git (.gitignore Vite его игнорирует по умолчанию). Это удобный способ держать локальные предпочтения отдельно от того, что лежит в репозитории. Подробнее — в документации Vite по env-файлам.Теперь:
- студент, у которого backend ещё не запущен, оставляет
VITE_USE_MOCKS=true— и приложение работает на моках, как раньше; - когда нужно проверить связку с реальным backend — переключает на
false(или просто запускает сVITE_USE_MOCKS=false npm run dev).
VITE_USE_MOCKS сделайте hard reload (DevTools → Application → Service Workers → Unregister, затем перезагрузка страницы), либо зайдите на сайт в режиме инкогнито.Что дальше
- В следующей лабораторной мы перенесём данные из памяти в Postgres через Prisma, упакуем backend в Docker, опишем общий
composeи поставим перед всем этим NGINX. - Маршруты
/api/usersи/api/healthсейчас используют in-memory массив — это шаблон, по которому вы (уже в рамках самостоятельной части) сможете дописать собственные ручки/api/rooms,/api/bookingsи т. п. под нужды итогового задания.