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.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics