<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Middleware как антипаттерн Node.js 2026: миграция на Fastify]]></title><description><![CDATA[<p dir="auto"><img src="/assets/uploads/files/99/bf/44/1774115131986-generated_1774115102180.webp" alt="Обложка: Middleware как антипаттерн в Node.js 2026: миграция с Express на Fastify с явной передачей контекста вместо неявного расширения request-response объектов" class=" img-fluid img-markdown" /></p>
<p dir="auto">Middleware в Express превратился в источник скрытых зависимостей и неявных модификаций объектов request-response. Это создаёт проблемы с трассировкой данных и тестированием. Современный подход — явная передача контекста вместо расширения глобальных объектов.</p>
<p dir="auto">В этой статье разберём, почему паттерн middleware стал антипаттерном, как это влияет на архитектуру приложений, и как мигрировать на Fastify с явной передачей контекста. Это касается каждого, кто серьёзно относится к качеству кода.</p>
<h2>Почему middleware Express это проблема</h2>
<p dir="auto">В Express middleware работает как <strong>цепочка ответственности</strong> из паттернов Gang of Four. Звучит красиво, но на практике каждый middleware может незаметно добавить данные в объект request или response. Ты пишешь обработчик маршрута и не знаешь, какие поля и методы уже добавили предыдущие middleware.</p>
<p dir="auto">Представь: логирование добавляет <code>req.id</code>, аутентификация добавляет <code>req.user</code>, валидация добавляет <code>req.validated</code>, а какой-то свой middleware добавляет <code>req.context</code>. Всё это неявно, в разных местах кода. Когда ты ищешь баг или пишешь тесты, ты должен помнить о всех этих скрытых расширениях. Это просто боль.</p>
<p dir="auto">Вот к чему это приводит на практике:</p>
<ul>
<li><strong>Неявные зависимости</strong>: функция зависит от данных в <code>req</code>, но это не видно из сигнатуры функции</li>
<li><strong>Сложность тестирования</strong>: нужно мокировать весь объект request со всеми добавленными полями</li>
<li><strong>Проблемы с типизацией</strong>: TypeScript не может проверить, какие свойства есть на request в каждой точке цепочки</li>
<li><strong>Трудность отладки</strong>: откуда взялось это поле на объекте? Какой middleware его добавил? Нужно искать по всему коду</li>
<li><strong>Хрупкость кода</strong>: добавил новый middleware — и вдруг что-то сломалось, потому что он перезаписал какое-то поле</li>
</ul>
<h2>От неявности к явности: как работает контекст</h2>
<p dir="auto">Вместо того чтобы полагаться на неявное расширение объектов, современный подход — это <strong>явная передача контекста</strong> как аргумента функции. Контекст содержит всё, что нужно обработчику: информацию о пользователе, логгер, конфигурацию, состояние запроса.</p>
<p dir="auto">Это не просто синтаксический сахар. Явная передача контекста делает код более предсказуемым и удобным для статического анализа. Когда ты видишь <code>async (ctx) =&gt; { }</code>, сразу понятно, какие данные нужны функции. IDE подскажет все доступные поля. TypeScript будет ругаться, если ты обращаешься к несуществующему полю.</p>
<p dir="auto">Как это выглядит в коде:</p>
<pre><code class="language-javascript">// Express: неявная зависимость
app.use((req, res, next) =&gt; {
  req.userId = extractUserId(req.headers);
  next();
});

app.get('/api/user', (req, res) =&gt; {
  // откуда взялся req.userId? Нужно помнить про middleware выше
  const user = getUserById(req.userId);
  res.json(user);
});

// Fastify: явная передача
app.addHook('preHandler', async (req, reply) =&gt; {
  req.userId = extractUserId(req.headers);
});

app.get('/api/user', async (req, reply) =&gt; {
  const user = getUserById(req.userId);
  return user;
});

// Лучший вариант: явный контекст
interface AppContext {
  userId: string;
  logger: Logger;
  db: Database;
}

const handler = async (ctx: AppContext) =&gt; {
  const user = await ctx.db.user.findById(ctx.userId);
  return user;
};
</code></pre>
<p dir="auto">Замечу: явность не значит, что нужно передавать параметры через 10 уровней функций. Контекст — это <strong>один объект</strong>, который содержит всё необходимое. Вместо <code>fn(req, res, next, logger, db, config, ...)</code> ты пишешь <code>fn(ctx)</code>. Это чище.</p>
<h2>Fastify vs Express: архитектурные различия</h2>
<p dir="auto">Fastify изначально разработан с учётом современных подходов. Его декоратор (hooks и плагины) работают иначе, чем Express middleware. Fastify не полагается на расширение объектов request-response, хотя технически это возможно.</p>
<p dir="auto">В Fastify ты можешь работать с контекстом через встроенные механизмы, которые делают это явным:</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Аспект</th>
<th>Express</th>
<th>Fastify</th>
</tr>
</thead>
<tbody>
<tr>
<td>Middleware</td>
<td>Расширяет req-res объекты</td>
<td>Hooks работают с контекстом</td>
</tr>
<tr>
<td>Типизация</td>
<td>Сложная типизация расширений req</td>
<td>Встроенная поддержка типов</td>
</tr>
<tr>
<td>Плагины</td>
<td>Модификация глобального приложения</td>
<td>Инкапсулированные области видимости</td>
</tr>
<tr>
<td>Контекст</td>
<td>Неявный, через req</td>
<td>Явный, через параметры хука</td>
</tr>
<tr>
<td>Декораторы</td>
<td>Добавляются на prototype</td>
<td>Регистрируются в приложении</td>
</tr>
</tbody>
</table>
<p dir="auto">Вот что особенно крутого в Fastify:</p>
<ul>
<li><strong>Области видимости плагинов</strong>: каждый плагин имеет свой scope, данные не утекают в другие части приложения</li>
<li><strong>Reply-объект чище</strong>: ты не расширяешь его, а просто возвращаешь данные</li>
<li><strong>Встроенная сериализация</strong>: Fastify сериализует ответ на основе JSON-схемы, никакой магии</li>
<li><strong>Контекст через декораторы</strong>: <code>fastify.decorate()</code> чётко показывает, что ты добавляешь в приложение</li>
</ul>
<h2>Пошаговая миграция с Express на Fastify</h2>
<p dir="auto">Миграция не означает переписывать всё с нуля. Можно делать это постепенно, заменяя middleware на явные обработчики контекста.</p>
<p dir="auto"><strong>Шаг 1: Создай интерфейс контекста</strong></p>
<p dir="auto">Определи, какие данные нужны в контексте запроса:</p>
<pre><code class="language-typescript">interface RequestContext {
  userId?: string;
  user?: User;
  requestId: string;
  logger: Logger;
  db: Database;
  cache: Cache;
}
</code></pre>
<p dir="auto"><strong>Шаг 2: Замени middleware на hooks в Fastify</strong></p>
<pre><code class="language-typescript">// Вместо Express middleware
app.addHook('preHandler', async (req, reply) =&gt; {
  req.context = {
    requestId: req.id,
    logger: logger.child({ requestId: req.id }),
    db: db,
    cache: cache
  };
});

// Аутентификация
app.addHook('preHandler', async (req, reply) =&gt; {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (token) {
    req.context.userId = decodeToken(token);
    req.context.user = await req.context.db.user.findById(req.context.userId);
  }
});
</code></pre>
<p dir="auto"><strong>Шаг 3: Обнови обработчики маршрутов</strong></p>
<pre><code class="language-typescript">// До миграции (Express)
app.get('/api/user/:id', async (req, res) =&gt; {
  const user = await req.db.user.findById(req.params.id);
  req.logger.info('User fetched', { userId: req.params.id });
  res.json(user);
});

// После миграции (Fastify)
app.get&lt;{ Params: { id: string } }&gt;('/api/user/:id', async (req, reply) =&gt; {
  const ctx = req.context as RequestContext;
  const user = await ctx.db.user.findById(req.params.id);
  ctx.logger.info('User fetched', { userId: req.params.id });
  return user;
});
</code></pre>
<p dir="auto"><strong>Шаг 4: Избавься от магии, используй явные функции</strong></p>
<p dir="auto">Итоговый паттерн: обработчик маршрута получает контекст явно, а логика находится в отдельных функциях:n</p>
<pre><code class="language-typescript">const fetchUser = async (ctx: RequestContext, userId: string) =&gt; {
  const user = await ctx.db.user.findById(userId);
  ctx.logger.info('User fetched', { userId });
  return user;
};

app.get&lt;{ Params: { id: string } }&gt;('/api/user/:id', async (req, reply) =&gt; {
  const user = await fetchUser(req.context, req.params.id);
  return user;
});
</code></pre>
<p dir="auto">Логика бизнеса теперь отделена от фреймворка. Функции легко тестировать, они не зависят от Express или Fastify.</p>
<h2>Реальные проблемы, которые решает явный контекст</h2>
<p dir="auto">Это не просто вопрос эстетики или чистоты кода. Явный контекст решает реальные проблемы:</p>
<p dir="auto"><strong>Проблема 1: Утечки данных между запросами</strong></p>
<p dir="auto">В Express легко случайно сохранить данные одного запроса в другом, если middleware использует замыкания или глобальные переменные. С явным контекстом каждый запрос имеет свой scope.</p>
<p dir="auto"><strong>Проблема 2: Сложность интеграции новых инструментов</strong></p>
<p dir="auto">Когда приходит новый инструмент (трейсинг, метрики, аналитика), нужно добавить ещё один middleware. С явным контекстом ты просто добавляешь новое поле в интерфейс и всё работает.</p>
<p dir="auto"><strong>Проблема 3: Тестирование обработчиков</strong></p>
<p dir="auto">Тестировать функцию, которая получает контекст аргументом, проще, чем мокировать весь объект request. Вот реальный пример:</p>
<pre><code class="language-typescript">// Тест с явным контекстом
const mockCtx: RequestContext = {
  userId: 'test-user',
  requestId: 'req-123',
  logger: mockLogger,
  db: mockDb,
  cache: mockCache
};

const result = await fetchUser(mockCtx, 'user-1');
assert(result.id === 'user-1');
</code></pre>
<p dir="auto">Сравни это с мокированием всего Express request-объекта — это кошмар.</p>
<p dir="auto"><strong>Проблема 4: Отладка и логирование</strong></p>
<p dir="auto">Знаешь, куда попадают данные? В контекст. Знаешь, где контекст создаётся? В hooks. Логирование становится простым и предсказуемым.</p>
<h2>Как структурировать приложение при явном контексте</h2>
<p dir="auto">После миграции на явный контекст логично переструктурировать приложение. Вот что рекомендуется:</p>
<ul>
<li><strong>Слой handlers</strong>: функции, которые получают контекст и параметры запроса, возвращают результат</li>
<li><strong>Слой services</strong>: бизнес-логика, которая использует контекст (логгер, БД, кэш)</li>
<li><strong>Слой data</strong>: работа с БД, кэшем, внешними API</li>
<li><strong>Слой utils</strong>: вспомогательные функции, которые не нуждаются в контексте</li>
</ul>
<p dir="auto">Пример структуры:</p>
<pre><code>src/
  handlers/
    user.ts       // обработчики маршрутов
  services/
    user.ts       // бизнес-логика
  data/
    user.ts       // запросы к БД
  types/
    context.ts    // интерфейсы контекста
  app.ts          // создание приложения
</code></pre>
<p dir="auto">Каждый слой знает только о слое ниже, не наоборот. Контекст проходит сверху вниз, неся всё необходимое.</p>
<h2>Оставить Express или переходить?</h2>
<p dir="auto">Если у тебя уже есть большой проект на Express, не обязательно срочно мигрировать. Но новые проекты лучше начинать с Fastify или другого современного фреймворка, который изначально поддерживает явный контекст.</p>
<p dir="auto">Есть ещё альтернативы: Oak для Deno, Hono для edge, Elysia для TypeScript. Все они построены на принципе явного контекста и не требуют манипуляций с объектами request-response.</p>
<p dir="auto">Главное — осознать, что неявная передача данных через расширение объектов это <strong>не масштабируется и не тестируется</strong>. Явность всегда выигрывает в долгосрочной перспективе.</p>
]]></description><link>https://forum.exlends.ru/topic/1821/middleware-kak-antipattern-node.js-2026-migraciya-na-fastify</link><generator>RSS for Node</generator><lastBuildDate>Sat, 17 Oct 2026 03:40:13 GMT</lastBuildDate><atom:link href="https://forum.exlends.ru/topic/1821.rss" rel="self" type="application/rss+xml"/><pubDate>Sat, 21 Mar 2026 17:45:32 GMT</pubDate><ttl>60</ttl></channel></rss>