Middleware: устройство и порядок вызова

Самый частый вопрос по Express: «Что такое middleware и в каком порядке они выполняются?» Ответ проверяется тут же задачкой на порядок вывода.

Middleware — функция с сигнатурой (req, res, next), которая получает управление до обработчика маршрута. Она может изменить req и res, завершить запрос ответом или передать управление дальше вызовом next().

Что проверяет интервьюер

Понимаете ли вы, что Express — это, по сути, массив функций и указатель на текущую. Отсюда следуют все практические вопросы: почему запрос «повис», почему req.body оказался undefined, почему обработчик ошибок не срабатывает и откуда берётся «Cannot set headers after they are sent».

Модель: стек и next()

Express хранит middleware в списке в порядке регистрации. Вызов next() сдвигает указатель на следующий элемент. Если next() не вызвать и не отправить ответ, запрос будет висеть до таймаута клиента. Механика ровно такая, и её легко воспроизвести на чистом синхронном JavaScript — этот пример можно запустить:

const stack = [];
const use = (fn) => stack.push(fn);

function handle(req, res) {
  let i = 0;
  function next() {
    const fn = stack[i++];
    if (!fn) return;
    fn(req, res, next);
  }
  next();
}

use((req, res, next) => {
  console.log('1 logger: пришёл запрос', req.url);
  next();
  console.log('1 logger: ответ сформирован, статус', res.status);
});

use((req, res, next) => {
  if (!req.user) {
    res.status = 401;
    console.log('2 auth: пользователь не авторизован, дальше не пускаем');
    return;                      // next() не вызываем — цепочка обрывается
  }
  next();
});

use((req, res, next) => {
  console.log('3 handler: сюда управление не дойдёт');
});

handle({ url: '/orders', user: null }, { status: 200 });

Вывод:

1 logger: пришёл запрос /orders
2 auth: пользователь не авторизован, дальше не пускаем
1 logger: ответ сформирован, статус 401

Обратите внимание на порядок: логгер получил управление первым и снова получил его последним — после того как next() вернулся. Эту схему называют «луковицей»: код до next() выполняется на пути внутрь, код после — на пути наружу. В Express это работает для синхронного кода; для замера времени ответа обычно вешают обработчик на событие res.on('finish'), потому что реальная отправка ответа асинхронна.

Порядок регистрации решает всё

const express = require('express');
const app = express();

app.use(express.json());                 // 1. разбор JSON — до маршрутов
app.use(requestId);                      // 2. свои сквозные middleware
app.use('/api/admin', requireAdmin);     // 3. middleware, ограниченный путём

app.get('/api/orders', listOrders);      // 4. маршруты
app.post('/api/orders', createOrders);

app.use((req, res) => res.status(404).json({ error: 'not found' }));   // 5. 404
app.use(errorHandler);                   // 6. обработчик ошибок — всегда последним

Если express.json() подключить после маршрутов, req.body в них будет undefined — классический баг новичка. Если обработчик ошибок зарегистрировать раньше маршрутов, он никогда не сработает: ошибки идут по стеку вперёд, а не назад.

Виды middleware

ВидПример
Глобальноеapp.use(cors()) — на все запросы
Привязанное к путиapp.use('/api', apiRouter)
Маршрутноеapp.get('/orders', auth, listOrders) — только для маршрута
Обработчик ошибок(err, req, res, next) — ровно четыре аргумента
Встроенное / стороннееexpress.static, helmet, morgan

Отдельно подчеркните на интервью: обработчик ошибок Express опознаёт по количеству аргументов. Уберёте неиспользуемый next из сигнатуры — функция станет обычным middleware и перестанет ловить ошибки.

Две классические ошибки

// 1. вызвали next() и продолжили отвечать
app.use((req, res, next) => {
  next();
  res.send('ой');           // маршрут уже отправил ответ -> ERR_HTTP_HEADERS_SENT
});

// 2. забыли и next(), и ответ — запрос висит до таймаута клиента
app.use((req, res, next) => {
  logger.info(req.url);     // тишина: управление никуда не передано
});

Ошибка ERR_HTTP_HEADERS_SENT означает ровно одно: на один запрос попытались ответить дважды. Обычно причина в забытом return перед res.json(...) или в вызове next() после отправки ответа.

Типичные ошибки кандидатов

  • Не могут объяснить, что делает next(), и говорят «переходит к следующему маршруту».
  • Не знают, что порядок регистрации определяет порядок выполнения.
  • Забывают про четырёхаргументную сигнатуру обработчика ошибок.
  • Не видят разницы между next() и next(err): второй перепрыгивает сразу к обработчику ошибок.

Как ответить кратко

Middleware — функция (req, res, next) в цепочке обработки запроса. Express хранит их в списке и выполняет строго в порядке регистрации: next() передаёт управление следующей, next(err) — сразу обработчику ошибок, а если не сделать ни того ни другого и не ответить, запрос повиснет. Парсеры вроде express.json() ставят до маршрутов, обработчик ошибок с четырьмя аргументами — последним. Двойной ответ на один запрос даёт ERR_HTTP_HEADERS_SENT.

Проверьте себя
1. Что произойдёт, если middleware не вызовет next() и не отправит ответ?
AExpress автоматически перейдёт к следующему middleware
BЗапрос повиснет до таймаута клиента
CВернётся 500 Internal Server Error
DСработает обработчик ошибок
2. Как Express отличает обработчик ошибок от обычного middleware?
AПо имени функции errorHandler
BПо регистрации через app.useError()
CПо количеству аргументов: (err, req, res, next) — ровно четыре
DПо тому, что он объявлен async
3. Почему req.body оказывается undefined, если app.use(express.json()) подключён после маршрутов?
AПотому что express.json работает только с POST-запросами
BПотому что middleware выполняются в порядке регистрации, и до маршрута парсер просто не успевает отработать
CПотому что нужен дополнительный вызов app.enable('json')
DПотому что express.json нельзя вызывать через app.use