FastifyMiddleTechnical
Как работает plugin encapsulation в Fastify и почему это важно для больших приложений?
Plugin encapsulation в Fastify: каждый register() создаёт дочерний контекст, где декорации, хуки и роуты изолированы от родителя. fastify-plugin пробивает инкапсуляцию, делая декорации видимыми в родительском scope. Это позволяет строить модульные приложения без глобального состояния.
Что такое plugin encapsulation
В Fastify каждый вызов app.register(plugin) создаёт новый дочерний контекст (scope). Этот контекст наследует всё от родителя, но изменения внутри (новые декорации, хуки, роуты) не видны в родительском или соседних scope. Это фундаментальный принцип архитектуры Fastify.
Демонстрация инкапсуляции
import Fastify from 'fastify';
const app = Fastify();
// Плагин A — изолированный scope
await app.register(async function pluginA(instance) {
instance.decorate('serviceA', { greet: () => 'Hello from A' });
// Работает внутри scope
instance.get('/a', async () => instance.serviceA.greet());
});
// Плагин B — другой scope
await app.register(async function pluginB(instance) {
// ОШИБКА: instance.serviceA === undefined
// Декорация из pluginA недоступна здесь
instance.get('/b', async () => {
// @ts-expect-error
return instance.serviceA?.greet() ?? 'not available';
});
});
await app.ready();
fastify-plugin: пробитие инкапсуляции
Когда плагин должен быть доступен во всём приложении (соединение с БД, конфиг, auth), оберните его в fastify-plugin:
import fp from 'fastify-plugin';
import fastifyPostgres from '@fastify/postgres';
// БЕЗ fp — декорация app.pg доступна только внутри этого scope
// С fp — декорация поднимается в родительский scope
export const dbPlugin = fp(
async function(app) {
await app.register(fastifyPostgres, {
connectionString: process.env.DATABASE_URL,
});
},
{
name: 'db',
fastify: '>=4.0.0',
dependencies: ['config'], // ожидает что плагин 'config' уже загружен
}
);
// Теперь app.pg доступен во всём приложении
Практический пример: auth scope
// Публичные роуты — без аутентификации
await app.register(async function publicRoutes(instance) {
instance.get('/health', async () => ({ status: 'ok' }));
instance.post('/auth/login', loginHandler);
});
// Защищённые роуты — с preHandler
await app.register(
async function protectedRoutes(instance) {
// Хук применяется ТОЛЬКО к роутам внутри этого scope
instance.addHook('preHandler', instance.authenticate);
instance.get('/users/me', getMeHandler);
instance.get('/orders', getOrdersHandler);
},
{ prefix: '/api/v1' }
);
Хук authenticate не затронет /health и /auth/login — они в другом scope.
Граф зависимостей плагинов
// Правильный порядок загрузки:
// config → db → redis → auth → routes
const app = Fastify();
await app.register(configPlugin); // декорирует app.config
await app.register(dbPlugin); // depends on 'config', декорирует app.pg
await app.register(redisPlugin); // depends on 'config'
await app.register(authPlugin); // depends on 'db'
await app.register(routesPlugin); // depends on 'auth', 'db', 'redis'
Почему это важно для больших приложений
- Изоляция middleware: хуки rate-limit или специфические преобразования применяются только к нужным роутам.
- Тестируемость: каждый domain-плагин можно протестировать отдельно, передав mock-экземпляр Fastify.
- Предотвращение коллизий: два плагина могут использовать одно имя декорации внутри своих scope без конфликта.
- Явные зависимости: через
dependenciesв fastify-plugin система стартует только если все зависимости доступны.
Подводные камни
- Забытый
fp()на плагине с декорацией: роуты в другом scope получатundefinedв runtime без ошибки при старте — только при первом запросе. - Двойная регистрация плагина с одним именем бросает ошибку
FST_ERR_DEC_ALREADY_PRESENT— используйтеfastify-pluginиnameдля идемпотентной регистрации. - Хук, зарегистрированный через
app.addHookв корневом scope, применяется ко ВСЕМ роутам включая дочерние — легко случайно добавить auth на публичные роуты. - Плагин внутри
registerбезawaitможет запуститься после старта сервера — всегдаawait app.register(). - TypeScript не знает о динамических декорациях — без augmentation
fastify.d.tsбудут ошибки типов или опасныеany. - При использовании
@fastify/autoloadпорядок загрузки алфавитный — если plugin B зависит от A, убедитесь черезdependenciesили именование файлов (01-a.ts, 02-b.ts).
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.