Middleware в Express: как делать правильно с первого раза
Порядок вызова next(), обработка ошибок, монтирование на путь — четыре подхода к middleware в Express 4.x и цена каждого из них.

Вы написали middleware, повесили его на приложение, а он либо не срабатывает, либо срабатывает не в том порядке, либо запрос виснет и клиент получает таймаут вместо ответа. Три самых частых причины — забытый next(), неверный порядок регистрации и middleware ошибок с тремя аргументами вместо четырёх. Разберём, как расставить их так, чтобы цепочка работала предсказуемо. Всё ниже — для Express 4.x; в Express 5.x поведение асинхронных ошибок отличается, об этом в конце.
Что такое цепочка middleware и почему порядок решает всё
Middleware в Express — это функция с сигнатурой (req, res, next). Express хранит их в массиве и вызывает по очереди, в том порядке, в котором вы их зарегистрировали через app.use() или методы маршрутов. Каждая функция либо завершает ответ (res.send(), res.json()), либо передаёт управление дальше вызовом next().
Ключевое следствие: если middleware A логирует запрос, а B проверяет авторизацию, и вы зарегистрировали B раньше A, — вы будете логировать только те запросы, что прошли авторизацию. Порядок регистрации — это порядок исполнения, других правил нет.
Middleware, который не вызвал next() и не завершил ответ, — это зависший запрос. Клиент ждёт до таймаута, соединение держится открытым. Самая частая причина мёртвого эндпоинта.
Рассмотрим четыре роли, в которых middleware ведёт себя по-разному, и цену ошибки в каждой.
Подход 1: глобальный middleware через app.use()
Срабатывает на каждый запрос независимо от маршрута. Сюда идут парсеры тела, логирование, CORS, установка заголовков.
const express = require('express');
const app = express();
// Парсер JSON — должен идти ДО маршрутов, которые читают req.body
app.use(express.json());
// Простой логгер: пишем метод и путь, затем передаём управление
app.use((req, res, next) => {
console.log(`${req.method} ${req.originalUrl}`);
next(); // без этого запрос зависнет
});
app.post('/users', (req, res) => {
// req.body уже распарсен, потому что express.json() выше
res.json({ received: req.body });
});
app.listen(3000);
Что выигрываете: единая точка для сквозной логики. Чем платите: любой глобальный middleware добавляет работу на каждый запрос, включая статику и health-check. Тяжёлую проверку (обращение к базе, разбор токена) не вешайте глобально — только на те маршруты, где она нужна.
Подход 2: middleware на конкретный путь или маршрут
Первым аргументом app.use() можно передать префикс пути. Тогда middleware сработает только для запросов, начинающихся с этого префикса.
// Проверка токена только для /api, публичные страницы её не трогают
function requireAuth(req, res, next) {
const token = req.headers['authorization'];
if (!token) {
return res.status(401).json({ error: 'no token' });
}
// здесь была бы реальная проверка токена
req.userId = 42;
next();
}
// Монтируем на префикс — сработает для /api/orders, /api/users и т.д.
app.use('/api', requireAuth);
app.get('/api/orders', (req, res) => {
res.json({ userId: req.userId, orders: [] });
});
// Публичный маршрут — requireAuth сюда не дотянется
app.get('/health', (req, res) => res.send('ok'));
Обратите внимание на return перед res.status(401). Без него после отправки ответа выполнение пойдёт дальше и, если ниже окажется next(), вы получите ошибку Cannot set headers after they are sent. Завершая ответ внутри условия, всегда выходите из функции.
Middleware для одного маршрута
Можно передать функцию прямо в определение маршрута, между путём и обработчиком:
const validateBody = (req, res, next) => {
if (!req.body.email) {
return res.status(400).json({ error: 'email required' });
}
next();
};
// validateBody выполнится только для этого POST
app.post('/subscribe', validateBody, (req, res) => {
res.json({ subscribed: req.body.email });
});
Подход 3: асинхронный middleware и ловушка забытого await
Если middleware делает запрос к базе или внешнему API, он асинхронный. В Express 4.x есть ловушка: если внутри async-функции выбросить исключение, оно не попадёт в обработчик ошибок автоматически. Express 4 ловит только синхронные ошибки и то, что вы явно передали в next(err).
const db = require('./db'); // ваш модуль доступа к БД
// НЕПРАВИЛЬНО в Express 4: reject промиса уйдёт в никуда,
// запрос зависнет, клиент дождётся таймаута
app.use('/api', async (req, res, next) => {
const user = await db.findUser(req.userId); // если бросит — тишина
req.user = user;
next();
});
// ПРАВИЛЬНО: оборачиваем в try/catch и передаём ошибку в next
app.use('/api', async (req, res, next) => {
try {
req.user = await db.findUser(req.userId);
next();
} catch (err) {
next(err); // уйдёт в error-handling middleware
}
});
Чтобы не писать try/catch в каждом асинхронном middleware, применяют обёртку. Она принимает async-функцию и возвращает обычный middleware, который ловит reject и вызывает next(err):
// Обёртка для асинхронных обработчиков
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
app.use('/api', asyncHandler(async (req, res, next) => {
req.user = await db.findUser(req.userId);
next();
}));
Готовый пакет с той же логикой — express-async-errors или express-async-handler; принцип у них общий. Проверьте актуальность и совместимость с вашей версией Express в их документации на npm.
Подход 4: middleware обработки ошибок — четыре аргумента
Обработчик ошибок отличается от обычного middleware ровно одним: у него четыре параметра — (err, req, res, next). Express распознаёт его по количеству аргументов функции. Уберёте первый — Express будет считать это обычным middleware, и ошибки в него не пойдут.
// Ставится ПОСЛЕДНИМ, после всех маршрутов и middleware
app.use((err, req, res, next) => {
console.error(err.stack);
const status = err.status || 500;
res.status(status).json({ error: err.message || 'internal error' });
});
Три правила для error-handler:
- Регистрируйте его после всех остальных
app.use()и маршрутов — иначе ошибки из маршрутов ниже до него не дойдут. - Сохраняйте все четыре параметра в сигнатуре, даже если
nextвнутри не используете. По их числу Express и понимает, что это обработчик ошибок. - Всегда завершайте ответ. Если ошибка пришла, а вы ничего не отправили и не вызвали
next(err), — запрос снова повиснет.
Когда что выбирать
| Роль middleware | Где регистрировать | Типичное применение |
|---|---|---|
| Глобальный | app.use(fn) вверху файла | Парсеры тела, CORS, логирование, заголовки безопасности |
| По префиксу | app.use('/api', fn) | Авторизация для группы маршрутов, версионирование API |
| Для маршрута | Аргумент в app.get/post(...) | Валидация тела конкретного эндпоинта, проверка прав |
| Ошибок | app.use((err, req, res, next)) в конце | Единый формат ответа об ошибке, логирование стека |
Критерий простой: чем шире область действия, тем легче должен быть middleware. Тяжёлое (обращения к БД, разбор токена, вызов внешнего сервиса) — как можно ближе к конкретному маршруту, а не глобально.
Что ломается чаще всего
- Забытый
next()— запрос виснет до таймаута. Проверьте, что каждая ветка middleware либо вызываетnext(), либо завершает ответ. - Парсер тела ниже маршрута —
req.bodyбудетundefined.express.json()должен идти выше маршрутов, которые читают тело. - Error-handler с тремя аргументами — Express примет его за обычный middleware, ошибки пройдут мимо. Оставляйте все четыре параметра.
- Async-ошибка без обёртки в Express 4 — reject промиса не попадёт в next(err), запрос повиснет. Используйте try/catch или asyncHandler.
Cannot set headers after they are sent— вы отправили ответ и продолжили выполнение. Ставьтеreturnпередres.send/res.jsonвнутри условий.
Что настроить дальше
Соберите middleware в отдельные файлы и подключайте через express.Router() — это разгружает главный файл и делает порядок регистрации явным для каждой группы маршрутов. Для продакшена добавьте helmet (заголовки безопасности) и настройте лимит размера тела в express.json({ limit: '...' }), чтобы большой запрос не съел память. Мерьте время прохождения цепочки: повесьте в начало и конец таймер и логируйте разницу — так вы увидите, какой middleware тормозит запрос под нагрузкой.
Агент-продажник: Скрипты B2B-продаж, обработка возражений по BANT и SPIN.
Спросить за 20 ₽Источники: Express — Using middleware (официальная документация), Express — Error handling
Частые вопросы
У меня Express 5, а не 4 — что изменится с async-ошибками?
Middleware не срабатывает вообще — куда смотреть?
Почему запрос виснет и клиент получает таймаут?
Можно ли поменять порядок middleware во время работы приложения?
Как передать данные из одного middleware в другой?
Error-handler ловит ошибку, но клиент всё равно получает пустой ответ — почему?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.