Middleware в Express: где писать своё, где брать готовое
Разбираем, когда middleware стоит писать руками, когда тянуть пакет из npm, а когда выносить логику из цепочки вовсе. С ценой каждого решения на примерах Express 4.x.

Задача звучит просто: добавить в приложение на Express проверку токена, логирование запросов и обработку ошибок. В npm по запросу «express auth» — сотни пакетов, но половина не обновлялась три года, а другая половина тянет двадцать транзитивных зависимостей ради одной функции. Написать своё — быстро, но легко получить c12 разных вариантов проверки токена в одном проекте. Разбираем, как выбирать между самописным middleware, готовым пакетом и выносом логики за пределы цепочки. Материал для Express 4.x; в 5.x поменялась обработка асинхронных ошибок — об этом ниже.
Что такое middleware и почему выбор вообще есть
Middleware в Express — это функция с сигнатурой (req, res, next), которую фреймворк вызывает по очереди для каждого запроса. Функция может изменить req или res, завершить ответ или передать управление дальше вызовом next(). Всё, что происходит между приходом запроса и отправкой ответа, — цепочка таких функций.
Именно поэтому выбор есть почти всегда. Одну и ту же задачу — скажем, разбор тела запроса или проверку прав — можно решить самописной функцией, готовым пакетом или вообще без middleware, внутри обработчика маршрута. У каждого пути своя цена.
Термин: транзитивная зависимость*
* Пакет, который вы напрямую не устанавливали, но он пришёл как зависимость установленного вами пакета. Один npm install может подтянуть десятки таких — и каждая расширяет поверхность для уязвимостей и ломающих обновлений.
Четыре подхода к одной задаче
Возьмём типичную задачу — проверку JWT в заголовке Authorization. Посмотрим четыре способа её встроить.
1. Самописный middleware
Функция на десять строк, использующая проверенную библиотеку для самой криптографии (например, jsonwebtoken), но всю обвязку вы пишете сами.
// Express 4.x, jsonwebtoken 9.x
const jwt = require('jsonwebtoken');
// Проверяет Bearer-токен и кладёт payload в req.user
function requireAuth(req, res, next) {
const header = req.headers.authorization || '';
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
if (!token) {
return res.status(401).json({ error: 'no_token' });
}
try {
// secret берётся из окружения, а не из кода
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch (err) {
// истёкший или подделанный токен
res.status(401).json({ error: 'invalid_token' });
}
}
module.exports = requireAuth;
Выигрываете: полный контроль, ноль лишних зависимостей поверх криптобиблиотеки, понятный формат ошибок под ваш API. Платите: сами следите за граничными случаями — регистр заголовка, пустой токен, обновление алгоритма подписи.
2. Готовый пакет-middleware
Например, express-jwt. Ставится, конфигурируется, встраивается одной строкой.
// express-jwt 8.x
const { expressjwt } = require('express-jwt');
app.use(
expressjwt({
secret: process.env.JWT_SECRET,
algorithms: ['HS256'],
})
);
Выигрываете: покрытые граничные случаи, чужой опыт эксплуатации, обновления безопасности. Платите: формат ошибок и место, куда кладётся payload, диктует пакет (в старых версиях это req.user, в новых req.auth — проверяйте документацию своей версии); плюс транзитивные зависимости и риск, что пакет забросят.
3. Middleware из фреймворка
Часть задач Express закрывает сам, без внешних пакетов. С версии 4.16 разбор JSON и форм встроен: express.json() и express.urlencoded(). До этого нужен был отдельный body-parser. Здесь правило простое: если задачу решает встроенный middleware — не тяните пакет.
// Express 4.16+, отдельный body-parser не нужен
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
4. Без middleware вовсе
Если проверка нужна на одном-двух маршрутах и завязана на бизнес-логику — иногда чище оставить её в обработчике, а не размазывать по цепочке. Middleware хорош для сквозных задач; логика, специфичная для одного эндпоинта, в цепочке только запутывает порядок вызовов.
Сравнение по цене
| Критерий | Самописный | Готовый пакет | Встроенный |
|---|---|---|---|
| Зависимости | минимум (только крипто) | пакет + транзитивные | ноль |
| Контроль формата ошибок | полный | ограничен пакетом | ограничен |
| Граничные случаи | на вас | покрыты сообществом | покрыты |
| Обновления безопасности | вручную | через npm | с версией Express |
| Порог входа | средний | низкий | низкий |
Когда что выбирать
- Встроенный — всегда, если задача им закрывается: разбор тела, статика через
express.static. Незачем ставить пакет ради того, что уже есть. - Готовый пакет — когда задача стандартная, но с кучей граничных случаев: CORS, rate-limiting, безопасные заголовки (
helmet), сессии. Тут чужой опыт эксплуатации дороже вашего контроля. - Самописный — когда нужен свой формат ошибок, специфичная логика авторизации или вы не хотите тянуть пакет ради десяти строк. Крипто и разбор форматов всё равно берите из проверенных библиотек, а не пишите с нуля.
- Без middleware — когда логика привязана к одному маршруту и не является сквозной.
Правило, экономящее часы отладки: middleware — это про то, что применяется к многим запросам одинаково. Как только вы пишете «if путь равен /admin» внутри глобального middleware, задумайтесь — возможно, это логика маршрута, а не цепочки.
Что ломается
Забытый next()
Самая частая ошибка в самописных middleware: функция ничего не ответила и не вызвала next(). Запрос повисает, клиент ждёт таймаута. Правило: либо завершите ответ (res.json, res.end), либо вызовите next() — ровно одно из двух на каждой ветке.
Порядок регистрации
Express вызывает middleware в порядке app.use. Если разбор тела зарегистрирован после маршрута, обработчик получит пустой req.body. Обработчик ошибок с четырьмя аргументами (err, req, res, next) должен идти последним — иначе он не поймает ошибки маршрутов, объявленных ниже.
Асинхронные ошибки в Express 4.x
В Express 4.x ошибка, выброшенная в асинхронном middleware, не попадает в обработчик ошибок автоматически — её нужно передать через next(err). Забудете — получите необработанный промис и, в худшем случае, падение процесса.
// Express 4.x — ошибки из await прокидываем руками
async function loadUser(req, res, next) {
try {
req.user = await db.findUser(req.params.id);
next();
} catch (err) {
next(err); // без этого ошибка потеряется
}
}
В Express 5.x поведение изменилось: отклонённые промисы из async-обработчиков передаются в обработчик ошибок автоматически. Если пишете под 5.x — проверьте документацию по обработке ошибок, ручной try/catch там уже не всегда нужен.
Что делать дальше
Пройдитесь по своему app.js и разложите каждый app.use по четырём категориям из таблицы. Кандидаты на удаление — пакеты, дублирующие встроенный функционал (например, body-parser при Express 4.16+). Дальше стоит настроить единый обработчик ошибок последним в цепочке и прогнать npm audit, чтобы увидеть, сколько уязвимостей приходит через транзитивные зависимости ваших middleware-пакетов. Инвалидация и обновление самих токенов — отдельная тема, здесь мы разбирали только встраивание проверки в цепочку.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: Express — Using middleware (официальная документация), Express — Migrating to Express 5
Частые вопросы
А если у меня Express 5.x, код с try/catch всё ещё нужен?
Middleware зарегистрировал, а req.body пустой — почему?
Стоит ли писать свою авторизацию вместо готового пакета?
Как понять, что middleware стал узким местом под нагрузкой?
Запрос повисает и не отвечает — что проверить в первую очередь?
Сколько транзитивных зависимостей нормально для middleware-пакета?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.