Композиция middleware

В Fresh middleware — это функции, которые встраиваются в процесс обработки HTTP-запроса между получением запроса сервером и формированием окончательного ответа. Они позволяют централизованно решать сквозные задачи: аутентификацию, логирование, работу с заголовками, локализацию, контроль доступа, модификацию запроса и ответа.

Middleware в Fresh работает на уровне Edge-совместимого сервера Deno и тесно интегрирован с роутингом фреймворка. Ключевая особенность — композиция middleware, то есть последовательное и предсказуемое объединение нескольких middleware в единый конвейер обработки запроса.


Базовая сигнатура middleware

Middleware в Fresh представляет собой функцию, принимающую объект Request и контекст MiddlewareHandlerContext и возвращающую Response или Promise<Response>.

import { MiddlewareHandlerContext } from "$fresh/server.ts";

export async function middleware(
  req: Request,
  ctx: MiddlewareHandlerContext,
) {
  return await ctx.next();
}

Ключевые элементы:

  • req — неизменяемый объект запроса
  • ctx.next() — вызов следующего middleware или обработчика маршрута
  • возвращаемое значение — итоговый HTTP-ответ

Middleware может:

  • завершить цепочку, вернув Response
  • передать управление дальше через ctx.next()
  • изменить ответ, полученный от следующего звена

Принцип композиции middleware

Композиция middleware в Fresh строится по принципу цепочки ответственности. Каждый middleware оборачивает последующий, образуя вложенную структуру вызовов.

Последовательность выполнения выглядит следующим образом:

  1. Первый middleware получает запрос
  2. Он вызывает ctx.next()
  3. Управление передаётся следующему middleware
  4. После завершения нижележащего middleware управление возвращается вверх по цепочке

Это позволяет реализовывать как pre-processing, так и post-processing логики.

Схематично:

Request
  ↓
Middleware A (до)
  ↓
Middleware B (до)
  ↓
Route handler
  ↑
Middleware B (после)
  ↑
Middleware A (после)
Response

Глобальные и маршрутные middleware

Fresh поддерживает два уровня middleware:

Глобальные middleware

Располагаются в файле routes/_middleware.ts и применяются ко всем маршрутам.

// routes/_middleware.ts
export async function middleware(req, ctx) {
  console.log(req.method, req.url);
  return await ctx.next();
}

Используются для:

  • логирования
  • установки общих заголовков
  • глобальной аутентификации
  • CSP, CORS, security headers

Middleware на уровне маршрута

Можно определить middleware внутри конкретного маршрута.

// routes/admin/_middleware.ts
export async function middleware(req, ctx) {
  if (!isAdmin(req)) {
    return new Response("Forbidden", { status: 403 });
  }
  return await ctx.next();
}

Такой middleware применяется только к маршрутам внутри директории admin.


Порядок композиции middleware

Порядок подключения middleware определяется структурой файловой системы:

  1. routes/_middleware.ts
  2. routes/segment/_middleware.ts
  3. routes/segment/sub/_middleware.ts
  4. Обработчик маршрута

Каждый уровень оборачивает следующий. Это гарантирует детерминированное выполнение без необходимости ручной конфигурации.

Пример структуры:

routes/
 ├─ _middleware.ts
 ├─ admin/
 │   ├─ _middleware.ts
 │   └─ dashboard.tsx

Цепочка вызовов для /admin/dashboard:

  1. Глобальный middleware
  2. Middleware из admin
  3. Обработчик dashboard.tsx

Управление потоком выполнения

Middleware может полностью контролировать дальнейшее выполнение цепочки.

Прерывание цепочки

Если middleware возвращает Response без вызова ctx.next(), дальнейшие middleware и обработчик маршрута не выполняются.

if (!authorized) {
  return new Response("Unauthorized", { status: 401 });
}

Оборачивание ответа

Частый приём — модификация ответа после выполнения следующего обработчика.

const response = await ctx.next();
response.headers.set("X-Powered-By", "Fresh");
return response;

Это позволяет добавлять:

  • заголовки
  • cookies
  • метаданные
  • telemetry-информацию

Передача данных через ctx.state

ctx.state — механизм передачи данных между middleware и обработчиками маршрутов.

ctx.state.user = user;
return await ctx.next();

В обработчике маршрута:

export const handler = {
  GET(req, ctx) {
    const user = ctx.state.user;
    return new Response(`Hello ${user.name}`);
  }
};

Особенности:

  • state живёт только в рамках одного запроса
  • данные не сериализуются
  • типизируется через generics

Типизация композиции middleware

Fresh позволяет строго типизировать ctx.state, что особенно важно при глубокой композиции middleware.

interface State {
  user?: User;
  locale?: string;
}

export async function middleware(
  req: Request,
  ctx: MiddlewareHandlerContext<State>,
) {
  ctx.state.locale = "ru";
  return await ctx.next();
}

Все последующие middleware и обработчики получают доступ к расширенному состоянию с гарантированной типобезопасностью.


Асинхронность и ошибки

Middleware полностью поддерживает асинхронность. Ошибки, выброшенные в любом middleware, всплывают вверх по цепочке.

try {
  return await ctx.next();
} catch (err) {
  return new Response("Internal Error", { status: 500 });
}

Это позволяет реализовать централизованную обработку ошибок на уровне глобального middleware.


Композиция как архитектурный инструмент

Композиция middleware в Fresh — не просто технический механизм, а архитектурный приём:

  • разделение ответственности
  • повторное использование логики
  • предсказуемый контроль потока
  • минимальная связанность компонентов

Благодаря файловой композиции и строгому порядку выполнения, middleware в Fresh остаётся прозрачным, масштабируемым и легко сопровождаемым даже в крупных проектах.