Почему middleware является антипаттерном? И как писать без него?nodejs-172

Почему middleware считается антипаттерном?

1. Проблемы с явностью потока выполнения

Middleware скрывает логику обработки запроса за цепочкой функций, что усложняет:

  • Отладку (трудно отследить порядок выполнения)
  • Понимание полного пути обработки запроса
  • Предсказание побочных эффектов

2. Нарушение принципа единой ответственности

Типичный middleware часто делает слишком много:

app.use((req, res, next) => {
  // Аутентификация
  // Логирование
  // Валидация
  // Преобразование данных
  next();
});

3. Проблемы с тестированием

Middleware сложно тестировать изолированно из-за:

  • Зависимости от next()
  • Модификации объектов req/res
  • Побочных эффектов

4. Неявные зависимости

Middleware часто полагается на:

  • Модифицированные предыдущими middleware поля req
  • Глобальное состояние
  • Неочевидный порядок регистрации

5. Сложность управления потоком ошибок

Обработка ошибок в middleware становится запутанной:

app.use((err, req, res, next) => {
  // Какие middleware вызвали ошибку?
  // Какой контекст был при ошибке?
});

Альтернативные подходы без middleware

1. Явные композиции обработчиков

Вместо:

app.use(authMiddleware);
app.use(loggingMiddleware);
app.get('/route', handler);

Используйте:

const withAuth = (handler) => async (req, res) => {
  const user = await authenticate(req);
  return handler({ ...req, user }, res);
};

const withLogging = (handler) => async (req, res) => {
  logRequest(req);
  return handler(req, res);
};

app.get('/route', withLogging(withAuth(handler)));

2. Декораторы/Обертки для маршрутов

function authenticated(handler) {
  return async (req, res) => {
    if (!req.headers.authorization) {
      return res.status(401).send();
    }
    return handler(req, res);
  };
}

app.get('/protected', authenticated(async (req, res) => {
  // Обработчик маршрута
}));

3. Использование классов обработчиков

class OrderHandler {
  constructor(dependencies) {
    this.authService = dependencies.authService;
    this.logger = dependencies.logger;
  }

  async handle(req, res) {
    try {
      this.logger.log(req);
      const user = await this.authService.verify(req);
      // Основная логика
    } catch (err) {
      this.handleError(err, res);
    }
  }
}

4. Функциональные pipeline

const processRequest = pipe(
  validateInput,
  authenticate,
  authorize,
  handleRequest,
  formatResponse
);

app.post('/api', async (req, res) => {
  const result = await processRequest(req);
  res.json(result);
});

5. Использование DI-контейнеров

// Настройка DI
const container = new Container();
container.register('authService', AuthService);
container.register('orderController', OrderController);

// Маршрут с явными зависимостями
app.get('/orders', (req, res) => {
  const controller = container.resolve('orderController');
  return controller.handle(req, res);
});

Практические преимущества альтернатив

  1. Лучшая тестируемость:
test('auth wrapper', async () => {
  const mockHandler = jest.fn();
  const wrapped = authenticated(mockHandler);
  await wrapped(mockReq, mockRes);
  expect(mockHandler).toHaveBeenCalled();
});
  1. Явные зависимости:
class UserController {
  constructor({ userService, authService }) {
    this.userService = userService;
    this.authService = authService;
  }
}
  1. Более понятный стек вызовов:
  • Легко проследить цепочку обработки
  • Четко видно преобразование данных
  • Явная обработка ошибок
  1. Лучшая производительность:
  • Меньше оберток вокруг каждого запроса
  • Возможность оптимизации горячих путей

Миграция с middleware на новые подходы

  1. Шаг 1: Выделите middleware в чистые функции
// Было:
app.use((req, res, next) => {
  req.user = parseUser(req);
  next();
});

// Стало:
const enrichRequestWithUser = (req) => ({
  ...req,
  user: parseUser(req)
});
  1. Шаг 2: Замените цепочки middleware на композиции
// Было:
app.use(auth);
app.use(logging);
app.get('/path', handler);

// Стало:
app.get('/path', pipe(
  enrichWithUser,
  logRequest,
  handler
));
  1. Шаг 3: Перенесите обработку ошибок в явные блоки
// Было:
app.use((err, req, res, next) => {...});

// Стало:
app.get('/path', async (req, res) => {
  try {
    // ...
  } catch (err) {
    errorHandler(err, res);
  }
});

Резюмируем:

отказ от middleware в пользу явных композиций, декораторов и DI делает приложение более предсказуемым, тестируемым и поддерживаемым. Хотя middleware удобен для быстрого старта, в долгосрочной перспективе явные подходы окупаются за счет лучшей архитектуры и понятности кода.