Middleware Pattern (антипаттерн по автору, 2026)

Цепочка обработчиков, мутирующих общий req/res. Похоже на Chain of Responsibility, но без концентрации ответственности. Согласно автору — антипаттерн для Node.js в 2026 году.

Проблема (которую middleware пытается решить)

Express/Connect: запрос проходит через серию обработчиков. Каждый что-то делает (логирование, auth, body parsing, validation). В конце цепочки — controller. Middleware добавляет сквозную функциональность (логирование, аутентификацию, валидацию, кэширование) без изменения основного кода.

app.use(logger);
app.use(auth);
app.use(bodyParser);
app.get('/users', controller);

Где исторически используется

  • HTTP-серверы: Express, Koa, Fastify
  • Redux: applyMiddleware для side-эффектов (redux-thunk, redux-saga)
  • Валидация и трансформация данных в пайплайне
  • Логирование и мониторинг запросов
  • Аутентификация и авторизация
  • GraphQL middleware

Базовая реализация цепочки

class Pipeline {
  constructor { this.middlewares = ; }
  use(fn) { this.middlewares.push(fn); return this; }

  execute(context) {
    let index = -1;
    const next = (i) => {
      if (i <= index) throw new Error('next called multiple times');
      index = i;
      const fn = this.middlewares[i];
      if (!fn) return;
      fn(context, () => next(i + 1));
    };
    next(0);
    return context;
  }
}

const pipeline = new Pipeline();
pipeline
  .use((ctx, next) => { ctx.value *= 2; next; })
  .use((ctx, next) => { ctx.value += 10; next; });
pipeline.execute({ value: 5 }); // value: 20

Express-стиль

class MiniExpress {
  constructor { this.stack = ; }
  use(fn) { this.stack.push(fn); }
  handle(req, res) {
    let idx = 0;
    const next = (err) => {
      if (err) return console.error('Error:', err);
      const fn = this.stack[idx++];
      if (fn) fn(req, res, next);
    };
    next;
  }
}

const app = new MiniExpress();
app.use((req, res, next) => { console.log(`${req.method} ${req.url}`); next; });
app.use((req, res, next) => {
  if (!req.headers.authorization) { res.status = 401; return; }
  next;
});
app.use((req, res, next) => { res.body = 'OK'; res.status = 200; });

Redux-стиль (currying)

const logger = store => next => action => {
  console.log('Action:', action.type);
  return next(action);
};

const thunk = store => next => action =>
  typeof action === 'function'
    ? action(store.dispatch, store.getState)
    : next(action);

Почему это антипаттерн (по автору)

1. Reference pollution

Middleware мутируют req и res:

function authMw(req, res, next) {
  req.user = await loadUser(req.headers.token); // мутация req
  sockets.add(req.socket);                       // утечка ссылки в коллекцию
  next;
}

Ссылки на req/res утекают в разные части приложения. Где-то ещё могут писать в req.socket — race conditions.

2. Shared state и неявное зацепление

Все middleware зацеплены друг за друга через общий мутабельный state (req, res). Зацепление высокое, неявное, опасное. Одна middleware ничего, а десятки — конфликтуют.

3. Зависимость от порядка

Перепутал порядок app.use(...) — проект не запускается. Авторы middleware не согласовали поведение между собой.

4. Размытые границы ответственности

Кто отвечает за auth? Один middleware или другой? Кто проверяет cookies — этот или вон тот? Утечки абстракции.

5. Невозможность изолированно тестировать

Middleware не работают в одиночку. Нужно запускать всю цепочку с реальным req/res.

6. Другие частые ошибки

  • Забыть вызвать next — цепочка обрывается молча.
  • Изменение req/res после next — состояние несогласованно.
  • Слишком длинная цепочка — сложно отлаживать.

Правильные альтернативы

Chain of Responsibility

// Каждый обработчик САМ решает: моё или нет
class AuthHandler {
  canHandle(req) { return req.path.startsWith('/api'); }
  handle(req) { return { user: this.authenticate(req) }; }
}

Один обработчик берёт ответственность, остальные не мутируют общее состояние.

Context Pattern

class RequestContext {
  #req; #res;
  constructor(req, res) { this.#req = req; this.#res = res; }
  getHeader(name) { return this.#req.headers[name]; }
  setBody(body) { /* безопасная запись */ }
}

Стримы недоступны напрямую. Любая операция оставляет контекст в консистентном состоянии.

NestJS-стиль

@Guard, @Pipe, @Interceptor — каждый имеет чёткую фазу и ответственность. Не мутируют общий объект.

Главные тезисы автора

  • «Middleware приводит к высокому зацеплению» через shared state.
  • «Reference pollution: ссылки на request/response утекают».
  • «Они неявно зацепляются через шареный стейт» — порядок становится критичен.
  • «Одна middleware ничего — десятки конфликтуют».
  • «Проект запускается перестановкой middleware» — типичный симптом.
  • «Chain of Responsibility безопаснее middleware» — есть концентрация ответственности.
  • «Если поверх Express вы используете Nest — вы изолированы от этих проблем».
  • Context Pattern — правильное решение: req/res в приватных полях, безопасный API наружу.
  • Metarhia metacom — пример системы без middleware-антипаттерна.

🎓 Источники

См. также