Свои и сторонние middleware
Свои middleware, привязка к маршруту, готовые пакеты вроде morgan и cors.
«Хороший middleware решает одну задачу и переиспользуется в десятке маршрутов.»
Теперь, когда механика ясна, научимся применять middleware прицельно: не только глобально, но и к конкретным маршрутам. Это позволяет, например, защитить только админские эндпоинты или логировать только платежи. Заодно познакомимся с экосистемой готовых middleware-пакетов.
Уровни применения
Middleware можно подключить тремя способами:
// 1. Глобально -- для всех запросов
app.use(logger);
// 2. Для конкретного пути и его подпутей
app.use('/admin', checkAdmin);
// 3. Только для одного маршрута (несколько обработчиков через запятую)
app.get('/secret', requireAuth, (req, res) => {
res.send('секретные данные');
});В третьем случае requireAuth выполнится перед основным обработчиком; если он не вызовет next() (например, ответит 401), до обработчика дело не дойдёт.
Пишем свой middleware
Полезный приём — middleware-фабрика: функция, возвращающая middleware с нужной настройкой. Так делают многие пакеты:
// фабрика: создаёт проверку конкретной роли
function requireRole(role) {
return function (req, res, next) {
if (req.user && req.user.role === role) {
next();
} else {
res.status(403).json({ error: 'нет доступа' });
}
};
}
app.delete('/users/:id', requireRole('admin'), (req, res) => {
res.status(204).end();
});Цепочка проверок в браузере
Смоделируем привязку middleware к маршруту: массив проверок выполняется до обработчика, и любая может прервать цепочку. Это сердце авторизации:
function runChain(ctx, chain) {
for (const fn of chain) {
const ok = fn(ctx);
if (!ok) {
console.log('Прервано на проверке:', fn.name);
return;
}
}
console.log('Доступ разрешён, выполняем обработчик');
}
const isLoggedIn = ctx => ctx.user != null;
const isAdmin = ctx => ctx.user && ctx.user.role === 'admin';
runChain({ user: { role: 'user' } }, [isLoggedIn, isAdmin]);
// Прервано на проверке: isAdmin
runChain({ user: { role: 'admin' } }, [isLoggedIn, isAdmin]);
// Доступ разрешён, выполняем обработчикГотовые пакеты
Не всё нужно писать самому. Популярные middleware из npm:
| Пакет | Назначение |
|---|---|
| morgan | логирование запросов |
| cors | разрешение кросс-доменных запросов |
| helmet | защитные HTTP-заголовки |
| express-rate-limit | ограничение частоты запросов |
Как работает под капотом
Когда ты передаёшь middleware в app.use('/admin', ...), Express добавляет слой с префиксом пути. При запросе он сначала проверяет, начинается ли url с этого префикса, и только потом запускает middleware. Несколько обработчиков в одном маршруте — это просто несколько слоёв подряд с одинаковым путём; next() ведёт от одного к следующему.
Частые ошибки
- Тяжёлая логика в глобальном middleware. Она выполнится на каждый запрос, включая статику. Привязывай дорогие проверки точечно.
- Забыть вернуть next() в фабрике. Лёгко написать проверку и забыть про "счастливый путь" — тогда все запросы зависнут.
- Слепо ставить пакеты. Каждый middleware — это и зависимость, и потенциальная уязвимость. Ставь только нужное.
Best practices
- Выноси повторяющуюся логику (проверка прав, валидация) в переиспользуемые middleware.
- Используй фабрики для параметризуемых проверок ролей и доступа.
- Применяй дорогие middleware только к тем маршрутам, где они реально нужны.
Итоги
Middleware можно подключать глобально, по пути и к одному маршруту. Фабрики делают их настраиваемыми, а экосистема npm закрывает типовые задачи. Дальше разберём особый вид middleware — обработчик ошибок, у которого на один аргумент больше.
Порядок — это контракт
Поскольку middleware выполняются строго по порядку подключения, их последовательность фактически становится частью архитектуры. Канонический порядок таков: сначала безопасность (helmet, cors, rate-limit), затем парсеры тела, потом логирование, дальше аутентификация, затем маршруты и в самом конце — обработчик ошибок. Перепутаешь — и, например, лимитер начнёт считать запросы уже после тяжёлого парсинга, теряя смысл, или логгер не увидит распознанного пользователя. Воспринимай список app.use(...) в начале файла как описание конвейера: его читают сверху вниз, и каждая строка добавляет одну станцию обработки.