Разделы документации
ОбзорБыстрый стартРедакции и возможностиМодели и поляРедактор контентаМедиатекаЛокализацияПубликация и работа командыAPI, SDK и генерация типовРасширения и инструментыРабочие проектыЗадания, вебхуки и наблюдаемостьАудит и управление даннымиСоветники и доверие к плагинамКорпоративный входПространства, квоты и масштабированиеCommerce и PortalПрава и безопасностьРазвёртывание и обновленияЛицензии и установка пакетовТекущие ограниченияПомощь и диагностикаДанные в кабинетеCore CMSDeveloper PlatformProduction UseWorkflowOperationsComplianceAI AssistantsPlugin TrustEnterprise IdentityEnterprise ScaleEnterprise DeploymentCommerce BundlePortal BundleNotty CMS DocumentationAuth & SecurityContent ModelingDeploymentEcosystem & Packaging ConventionsEditions and First-party ModulesExtensibilityGetting StartedMedia ManagementModule Extraction PathDraft & PublishUpgrade GuideWebhooks & IntegrationsCookbook: Blog with Next.jsCookbook: Custom PluginCookbook: Multilingual SiteOperations DocsBackup AutomationDeployment BlueprintsRunbook — Восстановление БД из бэкапаRunbook — Плановый деплойRunbook — Реакция на инцидентRunbook — Откат релизаRunbook — Горизонтальное масштабированиеRunbook — Ротация секретовRunbook — Major upgradeSecrets ManagementNotty CMS — Capability MapNotty Configuration ModelGenerated App ContractComponents and Dynamic ZonesMiddleware SystemPerformance & Scaling ToolkitDisaster Recovery PlaybookDistribution Model
Документация / Технический справочник

Middleware System

Техническое руководство из исходного проекта Notty. Примеры, параметры и эксплуатационные ограничения.

Все редакцииОбновлено 2026-09-30

Notty CMS поддерживает гибкую систему middleware/хуков для перехвата запросов и событий.

Generated App Contract

your-project/
├── notty.config.ts
└── src/
    ├── plugins/
    │   └── app.ts
    └── middlewares/
        └── index.ts

Generated apps регистрируют middleware через src/plugins/app.ts, который вызывает registerAppMiddlewares() из src/middlewares/index.ts.

Admin API Middleware

Перехват запросов к административному API.

// src/middlewares/index.ts
import { defineAdminMiddleware } from '@notty/core';

export function registerAppMiddlewares(ctx) {
  ctx.middleware.addAdmin(
    defineAdminMiddleware({
      name: 'log-admin-actions',
      description: 'Логирование действий админов',
      order: 10, // Выполняется раньше (меньше = раньше)

      // Паттерны маршрутов (glob-like)
      routes: 'admin:*', // Все admin маршруты
      // routes: ['admin:users:*', 'admin:roles:*'], // Только users и roles

      // HTTP методы (опционально)
      methods: ['POST', 'PUT', 'DELETE'],

      async handler(ctx, input, next) {
        console.log(`[Admin] ${ctx.adminUser?.username} -> ${ctx.routeName}`);

        // Вызываем следующий middleware/handler
        const result = await next();

        console.log(`[Admin] ${ctx.routeName} completed`);
        return result;
      },
    })
  );
}

Route Names

Admin маршруты используют формат admin:{resource}:{action}:

  • admin:users:list - GET /api/admin/users
  • admin:users:get - GET /api/admin/users/:id
  • admin:users:create - POST /api/admin/users
  • admin:users:update - PUT /api/admin/users/:id
  • admin:users:delete - DELETE /api/admin/users/:id
  • admin:auth:login - POST /api/admin/auth/login
  • admin:settings:get - GET /api/admin/settings

Content API Middleware

Перехват CRUD операций над контентом.

// src/middlewares/index.ts
import { defineContentMiddleware } from '@notty/core';

export function registerAppMiddlewares(ctx) {
  ctx.middleware.addContent(
    defineContentMiddleware({
      name: 'validate-slug',
      description: 'Автогенерация slug из title',

      // Фильтр по content types
      contentTypes: '*', // Все типы
      // contentTypes: ['article', 'page'], // Только article и page

      // Фильтр по операциям
      operations: ['create', 'update'],

      async handler(ctx, input, next) {
        const data = input as Record<string, unknown>;

        // Автогенерация slug если не указан
        if (data.title && !data.slug) {
          data.slug = (data.title as string)
            .toLowerCase()
            .replace(/[^a-z0-9]+/g, '-')
            .replace(/(^-|-$)/g, '');
        }

        return next();
      },
    })
  );
}

Операции Content API

  • create - POST /api/content/:type
  • read - GET /api/content/:type/:id
  • update - PUT /api/content/:type/:id
  • delete - DELETE /api/content/:type/:id
  • list - GET /api/content/:type
  • publish - PUT /api/content/:type/:id/publish
  • unpublish - PUT /api/content/:type/:id/unpublish
  • bulk-update - PATCH /api/content/:type/bulk-update
  • bulk-delete - DELETE /api/content/:type/bulk-delete

System Event Handlers

Реакция на системные события.

// src/middlewares/index.ts
import { defineSystemMiddleware } from '@notty/core';

export function registerAppMiddlewares(ctx) {
  ctx.middleware.addSystem(
    defineSystemMiddleware({
      name: 'notify-on-publish',
      description: 'Уведомление при публикации',

      events: ['content:afterPublish', 'content:afterCreate'],

      async handler(ctx) {
        if (ctx.event === 'content:afterPublish') {
          const { contentType, id, entry } = ctx.payload;
          console.log(`Published: ${contentType}#${id}`);

          // Отправка webhook, email и т.д.
        }
      },
    })
  );
}

Legacy Compatibility

Root-level middlewares/ discovery remains available for older projects as a compatibility path. For new generated apps use src/middlewares/index.ts together with src/plugins/app.ts.

Доступные события

Lifecycle:

  • app:start - Приложение запущено
  • app:stop - Приложение остановлено

Content CRUD:

  • content:beforeCreate - Перед созданием записи
  • content:afterCreate - После создания записи
  • content:beforeUpdate - Перед обновлением
  • content:afterUpdate - После обновления
  • content:beforeDelete - Перед удалением
  • content:afterDelete - После удаления
  • content:beforePublish - Перед публикацией
  • content:afterPublish - После публикации
  • content:beforeUnpublish - Перед снятия с публикации
  • content:afterUnpublish - После снятия с публикации

Content Read:

  • content:beforeFind - Перед получением одной записи
  • content:afterFind - После получения одной записи
  • content:beforeFindMany - Перед получением списка записей
  • content:afterFindMany - После получения списка записей
  • content:beforeCount - Перед подсчётом записей
  • content:afterCount - После подсчёта записей
  • content:beforeQuery - Универсальный хук перед любым read-запросом
  • content:afterQuery - Универсальный хук после любого read-запроса

Content Validation & Save:

  • content:beforeValidate - Перед валидацией данных
  • content:afterValidate - После валидации данных
  • content:beforeSave - Перед сохранением (create или update)
  • content:afterSave - После сохранения (create или update)

Schema:

  • schema:create - Схема создана
  • schema:update - Схема обновлена
  • schema:delete - Схема удалена
  • schema:sync - Синхронизация схем

Media:

  • media:upload - Файл загружен
  • media:delete - Файл удалён

Auth (Content API):

  • auth:beforeLogin - Перед попыткой входа
  • auth:afterLogin - Успешный вход пользователя
  • auth:loginFailed - Неудачная попытка входа
  • auth:logout - Выход пользователя
  • auth:register - Регистрация пользователя
  • auth:tokenRefresh - Обновление токена
  • auth:passwordReset - Запрос (stage: requested) / завершение (stage: completed) сброса пароля (/api/auth/forgot-password, /api/auth/reset-password)

Admin Auth:

  • admin:beforeLogin - Перед попыткой входа админа
  • admin:afterLogin - Успешный вход админа
  • admin:loginFailed - Неудачная попытка входа админа
  • admin:logout - Выход админа
  • admin:tokenRefresh - Обновление токена админа

Контексты

AdminMiddlewareContext

interface AdminMiddlewareContext {
  path: string;                    // URL path
  method: 'GET' | 'POST' | ...;   // HTTP метод
  headers: Record<string, string>; // Заголовки
  query: Record<string, string>;   // Query параметры
  params: Record<string, string>;  // Route параметры
  adminUser: AdminUserContext | null; // Текущий админ
  routeName: string;               // Имя маршрута (admin:users:list)
}

ContentMiddlewareContext

interface ContentMiddlewareContext {
  path: string;
  method: 'GET' | 'POST' | ...;
  headers: Record<string, string>;
  query: Record<string, string>;
  params: Record<string, string>;
  user: ContentUserContext | null; // Content user (если есть)
  adminUser: AdminUserContext | null; // Admin user (если запрос от админа)
  contentType: string;             // Тип контента
  operation: ContentOperation;     // Операция (create, read, etc.)
  entryId?: string | number;       // ID записи (для read/update/delete)
}

Прерывание запроса

Middleware может прервать цепочку и вернуть свой ответ:

export default defineContentMiddleware({
  name: 'block-delete',
  operations: ['delete'],

  async handler(ctx, input, next) {
    // Запретить удаление опубликованных записей
    if (ctx.entryId) {
      // Проверка...
      return {
        continue: false,
        response: {
          statusCode: 403,
          body: {
            success: false,
            message: 'Cannot delete published content',
          },
        },
      };
    }

    return next();
  },
});

Модификация данных

before* события позволяют модифицировать данные:

import { defineSystemMiddleware } from '@notty/core';

export function registerAppMiddlewares(ctx) {
  ctx.middleware.addSystem(
    defineSystemMiddleware({
      name: 'add-author',
      events: ['content:beforeCreate'],

      async handler(ctx) {
        const { data } = ctx.payload;

        // Добавляем автора автоматически
        data.createdBy = 'system';

        // Возвращаем модифицированный payload
        return { payload: { ...ctx.payload, data } };
      },
    })
  );
}

Порядок выполнения

Middleware выполняются в порядке order (по умолчанию 100):

// Выполнится первым
defineAdminMiddleware({ name: 'first', order: 10, ... });

// Выполнится вторым
defineAdminMiddleware({ name: 'second', order: 50, ... });

// Выполнится последним
defineAdminMiddleware({ name: 'last', order: 200, ... });

Отключение middleware

Для generated apps обычно достаточно удалить регистрацию из src/middlewares/index.ts.

Файлы с префиксом _ игнорируются только в legacy root-level loader:

middlewares/
└── admin/
    ├── active.ts       # Загружается
    └── _disabled.ts    # Игнорируется

Или через enabled: false:

defineAdminMiddleware({
  name: 'temporarily-disabled',
  enabled: false,
  ...
});

Единый конфиг файл

Для generated apps используйте единый registration entrypoint:

// src/middlewares/index.ts
import { defineAdminMiddleware, defineContentMiddleware } from '@notty/core';

export function registerAppMiddlewares(ctx) {
  ctx.middleware.addAdmin(defineAdminMiddleware({ name: 'admin-1', ... }));
  ctx.middleware.addAdmin(defineAdminMiddleware({ name: 'admin-2', ... }));
  ctx.middleware.addContent(defineContentMiddleware({ name: 'content-1', ... }));
}

Observability (Наблюдаемость)

Система middleware поддерживает observability для мониторинга и отладки.

RequestId и Tracing

Каждый запрос получает уникальный requestId, который прокидывается через всю цепочку middleware:

defineContentMiddleware({
  name: 'log-request',
  async handler(ctx, input, next) {
    console.log(`[${ctx.meta?.requestId}] Starting ${ctx.operation}`);
    const result = await next();
    console.log(`[${ctx.meta?.requestId}] Completed ${ctx.operation}`);
    return result;
  },
});

Заголовки трассировки

Поддерживаются стандартные заголовки:

  • X-Request-Id — уникальный ID запроса
  • X-Trace-Id — ID трассировки (для распределённых систем)
  • X-Parent-Span-Id — ID родительского span
  • X-Correlation-Id — альтернатива для Request-Id

Заголовки автоматически добавляются в ответ.

Middleware Observer

Для сбора метрик и логирования можно зарегистрировать observer:

import { setMiddlewareObserver } from '@notty/core';

setMiddlewareObserver({
  // Вызывается при завершении цепочки middleware
  onChainComplete(result) {
    console.log(`Chain completed in ${result.totalDuration}ms`);
    console.log(`Middlewares executed: ${result.middlewaresExecuted}`);
    console.log(`Request ID: ${result.requestId}`);

    // Детальные тайминги по каждому middleware
    for (const timing of result.timings) {
      console.log(`  ${timing.name}: ${timing.duration}ms`);
    }
  },

  // Вызывается при ошибке в middleware
  onMiddlewareError(error) {
    console.error(`Error in ${error.middlewareName}:`, error.message);
    console.error(`Request ID: ${error.requestId}`);
  },

  // Вызывается при завершении каждого middleware
  onMiddlewareComplete(timing, requestId) {
    if (timing.duration > 100) {
      console.warn(`Slow middleware: ${timing.name} took ${timing.duration}ms`);
    }
  },
});

Тайминги

В development режиме автоматически собираются тайминги каждого middleware:

interface MiddlewareTiming {
  name: string; // Имя middleware
  startTime: number; // Timestamp начала
  endTime: number; // Timestamp окончания
  duration: number; // Длительность в мс
  error?: boolean; // Была ли ошибка
  aborted?: boolean; // Была ли цепочка прервана
}

Error Boundaries

Ошибки в middleware обрабатываются и логируются автоматически:

  • Для Admin/Content middleware — ошибка прерывает цепочку и прокидывается дальше
  • Для System event handlers — ошибка логируется, но цепочка продолжает выполнение

Унифицированный формат ошибки:

interface MiddlewareError {
  middlewareName: string; // Имя middleware
  middlewareType: 'admin' | 'content' | 'system';
  message: string; // Сообщение ошибки
  stack?: string; // Stack trace (только в dev)
  code?: string; // Код ошибки
  statusCode?: number; // HTTP статус
  timestamp: Date; // Время ошибки
  requestId: string; // ID запроса
  metadata?: Record<string, unknown>; // Дополнительные данные
}

Интеграция с OpenTelemetry (опционально)

Для интеграции с OTEL используйте observer:

import { trace } from '@opentelemetry/api';
import { setMiddlewareObserver } from '@notty/core';

const tracer = trace.getTracer('notty-middleware');

setMiddlewareObserver({
  onChainComplete(result) {
    const span = tracer.startSpan('middleware-chain', {
      attributes: {
        'middleware.request_id': result.requestId,
        'middleware.duration_ms': result.totalDuration,
        'middleware.count': result.middlewaresExecuted,
        'middleware.has_error': result.hasError,
      },
    });
    span.end();
  },

  onMiddlewareError(error) {
    const span = tracer.startSpan('middleware-error', {
      attributes: {
        'middleware.name': error.middlewareName,
        'middleware.type': error.middlewareType,
        'error.message': error.message,
        request_id: error.requestId,
      },
    });
    span.setStatus({ code: 2, message: error.message });
    span.end();
  },
});

Источник: docs/middleware.md. Снимок документации исходного проекта. Технический справочник сохраняет язык оригинала.