Middleware в Node.js: как устроена цепочка обработчиков

Содержание

О проверке кода. Примеры не запускались — в отличие от материалов по Node.js с фактическим выводом. Код дан по документации Connect и Express. Говорим прямо, а не ставим бейдж «проверено».

Connect — библиотека, о которой сегодня почти не вспоминают, но её идея лежит в основе того, как в Node устроен любой веб-фреймворк. Разберём саму идею — она важнее библиотеки.

Что такое middleware

Middleware — это функция, через которую проходит запрос по пути к ответу. Их выстраивают в цепочку, и каждая делает ровно одно дело.

Представьте HTTP-запрос как посылку на конвейере. Первый пост проверяет отправителя (авторизация), второй записывает в журнал (логирование), третий распаковывает содержимое (разбор тела), и только потом посылка доходит до обработчика, который её обслужит.

// middleware — обычная функция трёх аргументов
function logger(req, res, next) {
  console.log(`${req.method} ${req.url}`);
  next();   // передаём запрос дальше по цепочке
}

Три параметра: req (запрос), res (ответ), next (передать управление следующему).

Ключевое: next() или ответ

У каждого middleware ровно два законных исхода:

function auth(req, res, next) {
  if (!req.headers.authorization) {
    res.writeHead(401).end('Нет доступа');   // ← завершаем цепочку
    return;
  }
  next();                                      // ← или пропускаем дальше
}

Либо next() — передать дальше, либо ответ — оборвать цепочку. Третьего нет, и здесь живёт главная ошибка.

Путь запроса через цепочку middleware
  1. 1 Пришёл запросОтдаётся первому middleware в цепочке
  2. 2 ЛогированиеЗаписали и вызвали next() — идём дальше
  3. 3 АвторизацияНет прав → res.end(401), цепочка обрывается
  4. 4 Разбор телаСобрали req.body, next()
  5. 5 Обработчик маршрутаФормирует и отправляет ответ

Забыли next() и не отправили ответ — запрос завис. Middleware получил управление и не отдал его никому. Клиент ждёт до таймаута, а вы ищете, почему «сервер молчит». Это причина номер один зависаний в Express-приложениях.

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

Middleware выполняются строго сверху вниз, в порядке подключения. Это не деталь, а логика работы.

app.use(logger);        // 1. сначала логируем всё
app.use(bodyParser);    // 2. потом разбираем тело
app.use(auth);          // 3. потом проверяем доступ
app.use(routes);        // 4. и только теперь маршруты

Переставьте auth и routes — и защита окажется после выдачи данных, то есть бесполезной. Поставьте bodyParser после обработчика, который читает req.body, — тело будет пустым.

Практическое правило: общее — раньше, специфичное — позже. Логирование и безопасность вверху, конкретные маршруты внизу.

Обработка ошибок: четвёртый аргумент

Особый вид middleware, который различают по числу параметров:

// обычный middleware — 3 аргумента
function normal(req, res, next) { }

// обработчик ошибок — 4 аргумента, первый err
function errorHandler(err, req, res, next) {
  console.error(err.stack);
  res.writeHead(500).end('Что-то пошло не так');
}

Express отличает обработчик ошибок именно по четырём параметрам — это не метафора, а буквально подсчёт fn.length. Такой middleware вызывается, только когда предыдущий передал ошибку: next(err) с аргументом.

Ставят его последним, чтобы он ловил всё, что упало выше по цепочке. Это тот же принцип, что обработка error в EventEmitter: одно место, куда стекаются все сбои.

Как это выглядит в Express

Connect почти никто не подключает напрямую, но его модель вы используете каждый раз, когда пишете на Express:

import express from 'express';
const app = express();

app.use(express.json());                 // разбор JSON — встроенный middleware
app.use((req, res, next) => {            // свой логгер
  console.log(req.method, req.url);
  next();
});
app.get('/api/users', (req, res) => {    // обработчик маршрута — тоже middleware
  res.json([{ name: 'Аня' }]);
});
app.use((err, req, res, next) => {       // обработчик ошибок — в конце
  res.status(500).json({ error: err.message });
});

Всё здесь — middleware. И express.json(), и логгер, и обработчик маршрута, и ловец ошибок — звенья одной цепочки. Это и есть наследие Connect: не библиотека, а способ думать о запросе как о потоке через обработчики.

Что было на этой странице в 2016 году

Первая версия вышла 31 декабря 2016 года и учила middleware на самом Connect: const connect = require('connect'), app.use(...), цепочка обработчиков. Для конца 2016 года — точно и по делу.

Что изменилось:

  • Connect как библиотека вышел из употребления. Его не берут в новые проекты — как Sails.js и Memcached, он не сломался, просто его роль занял Express. Показательно, что Express изначально и был построен поверх Connect.
  • Модель middleware, наоборот, стала фундаментом. То, что в 2016-м было особенностью конкретной библиотеки, сегодня — общий язык всего Node-веба: Express, Koa, Fastify — все про цепочки обработчиков.

Поэтому мы переписали урок не про Connect, а про саму идею middleware. Библиотека устарела — концепция стала стандартом, и именно её нужно понимать сегодня, на каком бы фреймворке вы ни писали.


Смежные темы: HTTP-сервер без фреймворков — что middleware надстраивает; Sails.js — та же история про смену выбора; события. Полный список — в справочнике по Node.js.

Частые вопросы

Что такое middleware простыми словами?
Функция, которая стоит между запросом и ответом и делает с ними что-то одно: проверяет авторизацию, логирует, парсит тело. Запрос проходит через цепочку таких функций по очереди, и каждая либо передаёт его дальше через next(), либо завершает.
Connect ещё используют?
Напрямую — почти нет. Но его идея жива: Express построен на той же модели middleware, и всё, что вы знаете про Connect, работает в Express. Сам Connect стал музейным экспонатом, концепция — стандартом.
Что будет, если забыть вызвать next()?
Запрос зависнет. Middleware получит управление и не передаст его дальше — клиент будет ждать ответа до таймаута. Забытый next() — причина номер один, почему «сервер не отвечает».
Почему порядок middleware важен?
Потому что они выполняются строго сверху вниз. Проверку авторизации нужно ставить ДО обработчика, который отдаёт данные, а разбор тела запроса — до того, кто это тело читает. Переставили — сломали логику.
Чем обработчик ошибок отличается от обычного middleware?
Числом аргументов. Обычный middleware принимает (req, res, next), обработчик ошибок — (err, req, res, next), четыре аргумента. Express различает их именно по количеству параметров.