Middleware в Express
Зачем нужно
Middleware -- функции-обработчики, которые имеют доступ к объектам request, response и функции next. Каждый HTTP-запрос проходит через цепочку middleware-ов как по конвейеру. Это позволяет разделить логику на изолированные, переиспользуемые блоки: парсинг тела, аутентификация, логирование, обработка ошибок, CORS, сжатие.
Где используется
- Парсинг тела запроса (JSON, form data, multipart)
- Аутентификация и авторизация
- Логирование запросов
- CORS-заголовки
- Безопасность (Helmet)
- Rate limiting
- Сжатие ответов (compression)
- Раздача статических файлов
- Обработка ошибок
Предпосылки
- Что такое Express — Express-приложение, req/res
- Роутинг — маршруты и Router
Анатомия middleware
// Middleware — это функция с 3 параметрами: (req, res, next)
function myMiddleware(req, res, next) {
// 1. Выполнить логику (модифицировать req/res, проверить данные)
console.log(`${req.method} ${req.url}`);
// 2. Один из вариантов:
// a) Передать управление дальше:
next;
// b) Завершить запрос:
// res.status(403).json({ error: 'Forbidden' });
// c) Передать ошибку:
// next(new Error('Something went wrong'));
}
Если middleware не вызывает next и не отправляет ответ —
запрос "зависнет" (клиент ждёт бесконечно)
app.use — подключение middleware
const express = require('express');
const app = express;
// Глобальный middleware — для ВСЕХ запросов
app.use((req, res, next) => {
req.requestTime = Date.now();
next;
});
// Middleware с путём — только для /api/*
app.use('/api', (req, res, next) => {
console.log('API request');
next;
});
// Несколько middleware подряд
app.use(express.json(), express.urlencoded({ extended: true }));
// Маршрут
app.get('/', (req, res) => {
res.json({ requestTime: req.requestTime });
});
app.listen(3000);
Встроенные middleware
express.json()
// Парсит тело запроса с Content-Type: application/json
app.use(express.json());
app.post('/api/users', (req, res) => {
console.log(req.body); // { name: 'Anna', email: 'anna@mail.com' }
res.json(req.body);
});
// С ограничением размера
app.use(express.json({ limit: '10kb' })); // макс 10 КБ
// Тело больше 10kb → 413 Payload Too Large
express.urlencoded
// Парсит тело из HTML-форм (Content-Type: application/x-www-form-urlencoded)
app.use(express.urlencoded({ extended: true }));
// extended: true — использует qs (вложенные объекты)
// extended: false — использует querystring (плоские данные)
// HTML: <form method="POST"><input name="username" value="anna"></form>
app.post('/login', (req, res) => {
console.log(req.body); // { username: 'anna' }
});
express.static
const path = require('path');
// Раздача статических файлов из директории
app.use(express.static(path.join(__dirname, 'public')));
// public/index.html → GET /index.html
// public/css/style.css → GET /css/style.css
// public/js/app.js → GET /js/app.js
// С префиксом пути
app.use('/static', express.static(path.join(__dirname, 'public')));
// public/style.css → GET /static/style.css
// Несколько директорий (проверяются по порядку)
app.use(express.static('public'));
app.use(express.static('uploads'));
// С опциями
app.use(express.static('public', {
maxAge: '1d', // кэширование на 1 день
index: 'index.html', // файл по умолчанию для директории
dotfiles: 'ignore' // не отдавать .gitignore и т.д.
}));
Свои middleware
Логирование
function logger(req, res, next) {
const start = Date.now();
// Сохраняем оригинальный res.end()
const originalEnd = res.end();
res.end() = function (...args) {
const duration = Date.now() - start;
console.log(
`${req.method} ${req.originalUrl} ${res.statusCode} - ${duration}ms`
);
originalEnd.apply(this, args);
};
next;
}
app.use(logger);
// GET /api/users 200 - 12ms
// POST /api/users 201 - 8ms
Аутентификация
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // прикрепляем данные пользователя к req
next;
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
}
// Защищённые маршруты
app.get('/api/profile', authenticate, (req, res) => {
res.json({ user: req.user });
});
// Или для всей группы маршрутов
const protectedRouter = express.Router;
protectedRouter.use(authenticate);
protectedRouter.get('/profile', (req, res) => { /* ... */ });
protectedRouter.get('/settings', (req, res) => { /* ... */ });
app.use('/api', protectedRouter);
Авторизация (проверка ролей)
function authorize(...roles) {
return (req, res, next) => {
// req.user уже установлен middleware authenticate
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next;
};
}
// Использование: только admin может удалять
app.delete('/api/users/:id',
authenticate,
authorize('admin'),
(req, res) => {
res.status(204).end();
}
);
// admin или moderator
app.put('/api/posts/:id',
authenticate,
authorize('admin', 'moderator'),
(req, res) => {
res.json({ updated: true });
}
);
Валидация
function validateBody(schema) {
return (req, res, next) => {
const errors = ;
for (const [field, rules] of Object.entries(schema)) {
const value = req.body[field];
if (rules.required && (value === undefined || value === '')) {
errors.push(`${field} is required`);
}
if (rules.type && value !== undefined && typeof value !== rules.type) {
errors.push(`${field} must be a ${rules.type}`);
}
if (rules.minLength && value && value.length < rules.minLength) {
errors.push(`${field} must be at least ${rules.minLength} characters`);
}
}
if (errors.length > 0) {
return res.status(400).json({ errors });
}
next;
};
}
// Использование
app.post('/api/users',
validateBody({
name: { required: true, type: 'string', minLength: 2 },
email: { required: true, type: 'string' },
age: { type: 'number' }
}),
(req, res) => {
res.status(201).json(req.body);
}
);
Rate Limiting
function rateLimit(windowMs, maxRequests) {
const requests = new Map();
return (req, res, next) => {
const ip = req.ip;
const now = Date.now();
if (!requests.has(ip)) {
requests.set(ip, );
}
const userRequests = requests.get(ip)
.filter(time => now - time < windowMs);
if (userRequests.length >= maxRequests) {
return res.status(429).json({
error: 'Too many requests',
retryAfter: Math.ceil((userRequests[0] + windowMs - now) / 1000)
});
}
userRequests.push(now);
requests.set(ip, userRequests);
next;
};
}
// Максимум 100 запросов за 15 минут
app.use('/api', rateLimit(15 * 60 * 1000, 100));
Error-handling middleware (4 параметра)
Middleware с 4 параметрами (err, req, res, next) Express распознаёт как обработчик ошибок.
// Определяется ПОСЛЕ всех маршрутов
function errorHandler(err, req, res, next) {
console.error(err.stack);
// Кастомная ошибка с кодом
if (err.statusCode) {
return res.status(err.statusCode).json({
error: err.message
});
}
// Ошибка валидации (mongoose, joi)
if (err.name === 'ValidationError') {
return res.status(400).json({
error: 'Validation Error',
details: err.message
});
}
// Все остальные ошибки — 500
res.status(500).json({
error: 'Internal Server Error',
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
}
// Использование
const app = express;
app.use(express.json());
app.get('/api/users/:id', async (req, res, next) => {
try {
const user = await findUser(req.params.id);
if (!user) {
const err = new Error('User not found');
err.statusCode = 404;
throw err;
}
res.json(user);
} catch (err) {
next(err); // передаём ошибку в error handler
}
});
// Error handler — ВСЕГДА последний app.use
app.use(errorHandler);
Async error wrapper
// Чтобы не писать try/catch в каждом маршруте:
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// Теперь ошибки из async функций автоматически попадают в error handler
app.get('/api/users', asyncHandler(async (req, res) => {
const users = await db.getUsers; // если упадёт — попадёт в errorHandler
res.json(users);
}));
app.get('/api/users/:id', asyncHandler(async (req, res) => {
const user = await db.getUser(req.params.id);
if (!user) {
const err = new Error('User not found');
err.statusCode = 404;
throw err; // автоматически попадёт в errorHandler через catch(next)
}
res.json(user);
}));
app.use(errorHandler);
// В Express 5 async ошибки обрабатываются автоматически (без asyncHandler)
Популярные third-party middleware
cors
npm install cors
const cors = require('cors');
// Разрешить все домены
app.use(cors);
// Конкретные домены
app.use(cors({
origin: ['http://localhost:3000', 'https://myapp.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true // разрешить cookies
}));
// Для конкретного маршрута
app.get('/api/public', cors, (req, res) => {
res.json({ data: 'public' });
});
helmet — безопасность
npm install helmet
const helmet = require('helmet');
// Устанавливает множество security-заголовков
app.use(helmet);
// Что делает helmet:
// X-Content-Type-Options: nosniff
// X-Frame-Options: SAMEORIGIN (защита от clickjacking)
// Strict-Transport-Security (HSTS)
// X-XSS-Protection
// Content-Security-Policy
// и другие
// Выборочная настройка
app.use(helmet({
contentSecurityPolicy: false, // отключить CSP
crossOriginEmbedderPolicy: false
}));
morgan — логирование
npm install morgan
const morgan = require('morgan');
// Предустановленные форматы
app.use(morgan('dev'));
// GET /api/users 200 12.345 ms - 234
app.use(morgan('combined'));
// ::1 - - [01/Jan/2024:12:00:00 +0000] "GET /api/users HTTP/1.1" 200 234
app.use(morgan('tiny'));
// GET /api/users 200 234 - 12.345 ms
// Логирование в файл
const fs = require('fs');
const path = require('path');
const accessLogStream = fs.createWriteStream(
path.join(__dirname, 'access.log'),
{ flags: 'a' } // append
);
app.use(morgan('combined', { stream: accessLogStream }));
Порядок middleware
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const app = express;
// 1. Безопасность (первым!)
app.use(helmet);
// 2. CORS
app.use(cors);
// 3. Логирование
app.use(morgan('dev'));
// 4. Парсинг тела
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 5. Статические файлы
app.use(express.static('public'));
// 6. Свои middleware (auth, rate limit)
app.use('/api', rateLimit(15 * 60 * 1000, 100));
// 7. Маршруты
app.use('/api/users', userRoutes);
app.use('/api/posts', postRoutes);
// 8. 404 handler
app.use((req, res) => {
res.status(404).json({ error: 'Not Found' });
});
// 9. Error handler (ВСЕГДА последний!)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.statusCode || 500).json({
error: err.message || 'Internal Server Error'
});
});
app.listen(3000);
Порядок важен:
helmet → cors → morgan → json → static → auth → routes → 404 → errors
- helmet до всего (заголовки безопасности)
- cors до маршрутов (preflight OPTIONS)
- json до маршрутов (парсинг req.body)
- static до маршрутов (быстрая раздача файлов)
- 404 после маршрутов (ловит всё что не совпало)
- error handler самый последний (ловит все ошибки)
Частые ошибки
- Забывают
next— запрос зависает бесконечно, клиент не получает ответ - Error handler без 4-х параметров — Express не распознаёт его как обработчик ошибок. Даже если
nextне используется, он должен быть в сигнатуре:(err, req, res, next) - Error handler до маршрутов — он не поймает ошибки из маршрутов, определённых после него
- express.json() после маршрутов —
req.bodyбудетundefined - Вызывают
nextИ отправляют ответ — двойной ответ,ERR_HTTP_HEADERS_SENT - Не оборачивают async в try/catch — необработанная ошибка крашит процесс (до Express 5)
Практика
- Написать middleware для логирования: метод, URL, время ответа в мс
- Создать middleware аутентификации, проверяющий header Authorization
- Реализовать middleware
authorize('admin'), возвращающий функцию для проверки роли - Настроить цепочку: helmet → cors → morgan → json → routes → 404 → errorHandler
- Написать
asyncHandlerдля автоматической передачи async-ошибок в error handler
Связанные темы
- Что такое Express — основы Express
- Роутинг — маршруты и Router
- http — HTTP-протокол
Ресурсы
🎓 Источник: Middleware антипатерн для Node.js
- 📅 2026-03-12 · YouTube · [Marp](../../../Documents/TimurShemsedinov/2026-03-12 — 🛑 Middleware антипатерн для Node.js у 2026 — Backend українською на 🐢 Express, N (Okr-d-vx6y0).md)
- 📅 2026-03-13 · YouTube (RU) · [Marp](../../../Documents/TimurShemsedinov/2026-03-13 — 🛑 Middleware это антипаттерн для Node.js в 2026 — Backend сервер на 🐢 Express, N (U5dWyukDMH4).md)
- Тезисы (критическая альтернативная позиция):
- Родовод: Connect → Express → Koa → Nest. Nest экранирует контракт middleware, но он остался
- Контракт
(req, res, next)ломает консистентность данных - Mixins to req/res провоцируют if-ы в каждом обработчике («если есть req.user, то...»)
- Reference pollution: сохранил
req.socketв глобальную Map → утечка ссылок на всю жизнь процесса - Race condition на стримах:
await db.queryв middleware → за это время response уже отослан другим middleware - Abstraction leak: middleware «знает» про конкретный фреймворк, бизнес-логика прибита к Express
- Тонкие контроллеры → толстые middleware-цепочки = high coupling
- Привязка к таймлайну:
awaitотдаёт управление другой части цепи → setTimeout меняет state response через 5с Shared userId в scope модуля = дыра в правах— классическая ошибка- Игнорирование ошибок в middleware: тест проходит, прод ломается
- Тяжело отлаживать: код перед
nextвыполняется ПОСЛЕ него (если await), переставляют middleware «методом тыка» - Замена: контекст-объект как фасад над req/res, передача в чистые функции; chain of responsibility вместо middleware; DDD-слои
- Цитата: «Middleware-цепочка — это GoTo: можно из любой точки попасть куда угодно, поэтому невозможно понять что будет дальше»
🎓 Источник: Безопасность приложений Node.js
- 📅 2019-11-28 · YouTube
- Тезисы:
- Middleware-говнокод: connection внутри handler-а, утечка между запросами
- Примеси к request/response (mixin) — главная причина утечек данных между юзерами в Node по сравнению с PHP