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-pressure | backpressure и health (503 под перегрузкой) |
@fastify/jwt, @fastify/auth | проверка токена, композиция стратегий |
@fastify/redis | общий клиент Redis на инстансе |
@fastify/swagger + swagger-ui | OpenAPI из схем роутов |
@fastify/cookie, @fastify/session | cookie и серверные сессии |
Каждый подключается одним 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, захватит значение первого запроса и будет
отдавать его всем.
db· asClass · SINGLETONrequestId· asFunction · SCOPEDarticleService· asClass · SCOPED · зависит от db, requestId
| запрос | db | requestId | articleService |
|---|---|---|---|
| жми «новый запрос» | |||
Это и есть главное правило скоупов: singleton не может зависеть от scoped. Иначе он замкнёт на себе устаревший контекст. Если сервис трогает данные запроса, он SCOPED.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.