Middleware в Express

Зачем нужно

Middleware -- функции-обработчики, которые имеют доступ к объектам request, response и функции next. Каждый HTTP-запрос проходит через цепочку middleware-ов как по конвейеру. Это позволяет разделить логику на изолированные, переиспользуемые блоки: парсинг тела, аутентификация, логирование, обработка ошибок, CORS, сжатие.

Где используется

  • Парсинг тела запроса (JSON, form data, multipart)
  • Аутентификация и авторизация
  • Логирование запросов
  • CORS-заголовки
  • Безопасность (Helmet)
  • Rate limiting
  • Сжатие ответов (compression)
  • Раздача статических файлов
  • Обработка ошибок

Предпосылки


Анатомия 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 самый последний (ловит все ошибки)

Частые ошибки

  1. Забывают next — запрос зависает бесконечно, клиент не получает ответ
  2. Error handler без 4-х параметров — Express не распознаёт его как обработчик ошибок. Даже если next не используется, он должен быть в сигнатуре: (err, req, res, next)
  3. Error handler до маршрутов — он не поймает ошибки из маршрутов, определённых после него
  4. express.json() после маршрутовreq.body будет undefined
  5. Вызывают next И отправляют ответ — двойной ответ, ERR_HTTP_HEADERS_SENT
  6. Не оборачивают async в try/catch — необработанная ошибка крашит процесс (до Express 5)

Практика

  1. Написать middleware для логирования: метод, URL, время ответа в мс
  2. Создать middleware аутентификации, проверяющий header Authorization
  3. Реализовать middleware authorize('admin'), возвращающий функцию для проверки роли
  4. Настроить цепочку: helmet → cors → morgan → json → routes → 404 → errorHandler
  5. Написать asyncHandler для автоматической передачи async-ошибок в error handler

Связанные темы

Ресурсы


🎓 Источник: 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