Раздел 8 · Frameworks и Libs

Fastify: экосистема и production

senior~30 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Fastify: экосистема и production

Четыре урока дали фундамент: плагины, инкапсуляция, декораторы, хуки, схемы. Теперь соберём из этого боевой модульный API: добавим экосистему @fastify/* для безопасности и наблюдаемости, DI-контейнер рядом с плагинами, доменную раскладку, OpenAPI из схем, тесты без подъёма порта и аккуратный останов. Это финал Fastify-трека: то, что отличает учебный сервер от продакшена.

1. Экосистема @fastify/*

Fastify намеренно маленький в ядре: всё остальное живёт плагинами. Боевой проект собирают из официальных @fastify/*, каждый плагин закрывает свою сквозную задачу. Карта (полная на fastify.dev/ecosystem):

ПлагинЗачем
@fastify/corsполитика CORS для браузерных клиентов
@fastify/helmetбезопасные заголовки ответа
@fastify/rate-limitлимит запросов на клиента (429 при превышении)
@fastify/under-pressurebackpressure и health (503 под перегрузкой)
@fastify/jwt, @fastify/authпроверка токена, композиция стратегий
@fastify/redisобщий клиент Redis на инстансе
@fastify/swagger + swagger-uiOpenAPI из схем роутов
@fastify/cookie, @fastify/sessioncookie и серверные сессии

Каждый подключается одним register, и почти все, через fp внутри, то есть кладут декораторы на инстанс (например, app.jwt), доступные всему приложению. Это ровно механизм из урока 02: плагин-сервис, осознанно ломающий инкапсуляцию.

1.1 under-pressure: деградация под нагрузкой

Отдельно стоит выделить under-pressure. Он мониторит задержку event loop и память, и при перегрузке возвращает 503, не пуская новые запросы. Это backpressure на уровне приложения: лучше честно отказать части запросов, чем захлебнуться и упасть целиком. Тема перекликается с уроком 06-node про масштабирование: сервис должен деградировать предсказуемо.

await app.register(underPressure, {
  maxEventLoopDelay: 200, // мс
  maxHeapUsedBytes: 1_000_000_000,
  healthCheck: async () => ({ db: await pingDatabase() }),
  exposeStatusRoute: '/health',
});

2. DI-контейнер: awilix рядом с плагинами

Плагины и инкапсуляция, это уже форма организации зависимостей. Но для доменной логики (сервисы, репозитории, их связи) удобен полноценный DI-контейнер. В экосистеме Fastify это awilix через @fastify/awilix. Контейнер живёт рядом с плагинами, а не вместо них: плагины задают границы и инфраструктуру, awilix собирает доменный граф.

import { diContainer, fastifyAwilixPlugin } from '@fastify/awilix';
import { asClass, asFunction, asValue, Lifetime } from 'awilix';

await app.register(fastifyAwilixPlugin, { disposeOnClose: true });

diContainer.register({
  db: asValue(databaseClient), // готовое значение
  articleRepository: asClass(ArticleRepository, { lifetime: Lifetime.SINGLETON }),
  articleService: asClass(ArticleService, { lifetime: Lifetime.SCOPED }),
});

// в обработчике достаём из per-request scope
app.get('/articles', async (request) => {
  const service = request.diScope.resolve('articleService');
  return service.list();
});

Способы регистрации:

  • asClass(C), контейнер сделает new C(...), передав зависимости по именам параметров конструктора;
  • asFunction(f), вызовет фабрику f(deps);
  • asValue(v), отдаст готовое значение как есть.

2.1 Скоупы: SINGLETON против SCOPED

Главное решение при регистрации, время жизни. Cradle отдаёт инстанс по имени, а скоуп определяет, переиспользуется ли он:

  • SINGLETON, один инстанс на всё приложение. Так регистрируют дорогие ресурсы: пул соединений, клиент Redis.
  • SCOPED, свежий инстанс на каждый запрос (request.diScope). Так регистрируют сервисы, которым нужен контекст запроса (текущий пользователь, reqId).

Покрути модель: жми «новый запрос» и смотри на instance id. Синглтон db переиспользуется (один id на все запросы), scoped-сервис создаётся заново. Переключи articleService в SINGLETON и увидишь ловушку: синглтон, зависящий от scoped requestId, захватит значение первого запроса и будет отдавать его всем.

awilix: SINGLETON против SCOPED
регистрации в контейнере
  • db · asClass · SINGLETON
  • requestId · asFunction · SCOPED
  • articleService · asClass · SCOPED · зависит от db, requestId
resolve по запросам (instance id)
запросdbrequestIdarticleService
жми «новый запрос»

Это и есть главное правило скоупов: singleton не может зависеть от scoped. Иначе он замкнёт на себе устаревший контекст. Если сервис трогает данные запроса, он SCOPED.

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.