Форум программистов, компьютерный форум, киберфорум
Reangularity
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

Десять Middleware Node.js для эффективного кодинга

Запись от Reangularity размещена 18.07.2025 в 19:05
Показов 2568 Комментарии 0

Нажмите на изображение для увеличения
Название: Десять Middleware Node.js для эффективного кодинга.jpg
Просмотров: 255
Размер:	185.4 Кб
ID:	11001
Когда я только начинал работать с Node.js, количество пакетов в npm меня буквально парализовало. Сегодня их больше 1,3 миллиона — попробуй разберись, что стоит твоего внимания, а что нет. Я потратил несколько лет, чтоб методом проб и ошибок отсеять золото от шелухи, особенно когда дело касается middleware-компонентов.

Middleware в экосистеме Node.js — это как малярный скотч в ремонте: вроде мелочь, но без него всё разваливается. Это промежуточные обработчики, которые сидят между запросом и ответом, выполняя критически важные функции: авторизацию, логирование, парсинг тел запросов и даже защиту от хакерских атак. Особенно остро проблема выбора встает, когда разрабатываешь что-то серьезное для продакшена. На кону не только твое время, но и безопасность приложения, его производительность, а иногда и судьба всего проекта. В таких ситуациях хорошо проверенное решение становится на вес золота.

Express.js и его middleware экосистема



Если вы хоть раз разрабатывали на Node.js, то наверняка сталкивались с Express.js. Этот фреймворк стал своего рода стандартом де-факто для создания веб-приложений и API. И не зря — его минималистичный подход и потрясающая гибкость делают его идеальной основой практически для любого проекта. Суть Express в том, что он сам по себе довольно прост, но предоставляет архитектуру, позволяющую легко расширять функциональность через middleware. По своей природе middleware — это просто функции, имеющие доступ к объектам запроса (req), ответа (res) и следующему middleware в цепочке (next). Это может показаться банальным, но эта простая концепция — ключ к невероятной мощи Express.

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const express = require('express');
const app = express();
 
// Простейший middleware
app.use((req, res, next) => {
  console.log('Время запроса:', Date.now());
  next(); // Не забывайте вызывать next(), иначе запрос зависнет!
});
 
app.get('/', (req, res) => {
  res.send('Привет, мир!');
});
 
app.listen(3000);
Я обычно представляю middleware как конвейерную линию на заводе. Каждая функция выполняет свою маленькую задачу, а затем передает "деталь" (запрос) дальше по конвейеру. Одна функция может добавлять заголовки безопасности, другая — парсить JSON в теле запроса, третья — проверять авторизацию, и так далее.

Порядок применения middleware критически важен. Представьте, что вы хотите проверить JWT-токен в запросе, но middleware, парсящий заголовки, стоит после авторизации — ничего не сработает! Такие баги могут быть очень коварными, особенно в больших приложениях. Есть три основных способа применения middleware в Express:

1. Глобально для всех маршрутов: app.use(middleware)
2. Для конкретного пути: app.use('/api', middleware)
3. Для конкретного HTTP-метода: app.get('/user', middleware, handler)

С ростом приложения растет и количество middleware. В одном из моих проектов мы использовали больше 20 различных middleware-компонентов, и это не считая маршрутизацию и обработчики ошибок. Без четкой структуры код быстро превратился в спагетти. Чтобы избежать этой проблемы, я пришел к определенному подходу структурирования middleware. Обычно я группирую их по функциональности и применяю в следующем порядке:

1. Инфраструктурные (логирование, парсинг тела, сжатие).
2. Безопасность (helmet, cors, rate-limiting).
3. Бизнес-логика (авторизация, валидация).
4. Обработчики ошибок (всегда в самом конце).

Одной из интересных особенностей Express является поддержка асинхронных middleware. Начиная с Express 5 (который, к сожалению, все еще в бете), фреймворк автоматически обрабатывает промисы, возвращаемые middleware. Но в Express 4 нужно быть осторожным — если вы используете async/await, не забудьте обернуть код в try/catch и вызвать next(error) в случае ошибки:

JavaScript
1
2
3
4
5
6
7
8
9
app.use(async (req, res, next) => {
  try {
    const result = await someAsyncOperation();
    req.result = result;
    next();
  } catch (error) {
    next(error); // Передаем ошибку Express
  }
});
С интеграцией TypeScript всё становится еще интереснее. Типизированные middleware существенно повышают надежность кода. Однако сама структура типов в Express не всегда интуитивно понятна. Я часто использую такой подход:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Request, Response, NextFunction } from 'express';
 
interface AuthenticatedRequest extends Request {
  user?: {
    id: string;
    role: string;
  };
}
 
const authMiddleware = (
  req: AuthenticatedRequest,
  res: Response,
  next: NextFunction
) => {
  // Логика авторизации
  req.user = { id: '123', role: 'admin' }; // Добавляем данные пользователя к запросу
  next();
};
Такое расширение типов позволяет безопасно обращаться к пользовательским свойствам в последующих обработчиках.
Один из сложных вопросов, с которым я сталкивался — как тестировать middleware? Напрямую вызывать функцию не всегда корректно, потому что реальный контекст выполнения отличается. В итоге я остановился на таком подходе:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
const middleware = require('./my-middleware');
const httpMocks = require('node-mocks-http');
 
test('middleware добавляет поле user к запросу', () => {
  const req = httpMocks.createRequest();
  const res = httpMocks.createResponse();
  const next = jest.fn();
  
  middleware(req, res, next);
  
  expect(next).toHaveBeenCalled();
  expect(req.user).toBeDefined();
});
Библиотека node-mocks-http позволяет эмулировать объекты запроса и ответа Express, что делает тестирование гораздо более реалистичным.

Почему CORS-middleware не ставит хедеры для другого middleware в Express?
Упрощенный код: const app = express(); app.use(express.json()); // CORS middleware...

Nuxtjs как в middleware обратиться к store
Здравствуйте!!! Помогите понять как подключить эту мидлвару. Я хочу проверять юзера из стора. ...

Middleware проверка токена
Коллеги, добрый вечер. Уже начинает подташнивать от перечитанных примеров для авторизации в...

Express middleware запускается несколько раз за один запрос
Здраствуйте, друзья! Где то у книге читал что если размещать промежуточною функцию до розмищения...


Helmet - защита без лишних телодвижений



Безопасность веб-приложений — это та сфера, где лучше перебдеть, чем недобдеть. Особенно когда речь идет о заголовках HTTP, которые могут как усилить защиту вашего приложения, так и раскрыть потенциальным атакующим слишком много информации. И тут на сцену выходит Helmet — один из тех middleware, без которого я не представляю ни одного продакшен-приложения на Express.

По сути, Helmet — это набор небольших middleware-функций, каждая из которых устанавливает определенный HTTP-заголовок безопасности. Что меня в нем подкупает — простота использования при мощном эффекте. Достаточно подключить его в начале вашего приложения:

JavaScript
1
2
3
4
5
6
7
8
const express = require('express');
const helmet = require('helmet');
const app = express();
 
// Применяем Helmet со стандартными настройками
app.use(helmet());
 
// Остальной код приложения
И вуаля — ваше приложение уже защищено от нескольких типов атак! Но что конкретно делает Helmet? Он устанавливает следующие заголовки:

X-XSS-Protection — защита от XSS-атак в старых браузерах,
Content-Security-Policy — контроль ресурсов, которые может загружать браузер,
X-Frame-Options — защита от кликджекинга (когда ваш сайт загружают во фрейм),
X-Content-Type-Options — предотвращает MIME-снифинг,
И еще целый ряд других полезных заголовков

Особо хочу отметить, что Helmet также убирает заголовок X-Powered-By, который по умолчанию добавляет Express. Зачем раскрывать технологический стек вашего приложения потенциальным злоумышленникам? Я вообще сторонник минимального раскрытия информации. Как-то раз я унаследовал проект, где была уязвимость к кликджекингу — конкуренты встраивали сайт во фрейм на своей странице и перехватывали клики пользователей. Простое добавление Helmet решило проблему за пару минут, без необходимости разбираться в тонкостях HTTP-заголовков.

Конечно, не всегда стандартных настроек достаточно. Для тонкой настройки Helmet предлагает возможность конфигурирования каждого заголовка отдельно:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "trusted-cdn.com"],
      styleSrc: ["'self'", "'unsafe-inline'", "trusted-cdn.com"],
      imgSrc: ["'self'", "data:", "images.cdn.com"],
      connectSrc: ["'self'", "api.myservice.com"]
    }
  },
  crossOriginEmbedderPolicy: false,
  // Другие настройки
}));
Настройка Content Security Policy (CSP) заслуживает отдельного разговора. Это мощный механизм безопасности, который контролирует, какие ресурсы может загружать браузер. Для современных SPA-приложений на React, Angular или Vue правильная настройка CSP может быть сложной задачей, особенно если вы используете inline-стили или eval в своем коде.

Я лично столкнулся с этим, когда разрабатывал React-приложение с Material-UI. Библиотека использовала inline-стили, что нарушало стандартную политику CSP. Пришлось изрядно повозиться с настройками, чтобы найти баланс между безопасностью и работоспособностью. Вот мой рабочий вариант CSP для типичного React-приложения:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'unsafe-eval'"], // Нужно для некоторых бандлеров
    styleSrc: ["'self'", "'unsafe-inline'"], // Для стилей React-компонентов
    imgSrc: ["'self'", "data:", "blob:"],
    connectSrc: ["'self'", "api.mycompany.com"], // Ваш API
    fontSrc: ["'self'", "fonts.gstatic.com"],
    objectSrc: ["'none'"],
    upgradeInsecureRequests: [],
  },
}));
Кстати, для отладки CSP рекомендую использовать режим отчетов. Это позволяет собирать информацию о нарушениях политики, не блокируя ресурсы:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
app.use(helmet.contentSecurityPolicy({
  directives: {
    // ... ваши обычные директивы ...
    reportUri: '/csp-violation-report'
  },
  reportOnly: true // Включает режим "только отчеты"
}));
 
// Эндпоинт для сбора отчетов
app.post('/csp-violation-report', (req, res) => {
  console.log('CSP нарушение:', req.body);
  res.status(204).end();
});
За пределами CSP, Helmet предлагает ещё ряд полезных функций. Например, helmet.hsts() устанавливает заголовок Strict-Transport-Security, который говорит браузеру всегда использовать HTTPS для вашего домена. Это критически важно для защиты от атак типа man-in-the-middle.

Нельзя не упомянуть про защиту от XSS-атак. Хотя современные фреймворки (React, Vue) уже имеют встроенные механизмы экранирования, дополнительный уровень защиты никогда не помешает. Helmet устанавливает заголовок X-XSS-Protection: 1; mode=block в старых браузерах и помогает настроить CSP для новых. В своей практике я заметил, что Helmet особенно эффективен в связке с другими мерами безопасности. Например, комбинация Helmet + rate-limiting + CORS дает хороший базовый уровень защиты с минимальными усилиями.

Что действительно ценно — Helmet постоянно обновляется, следуя за эволюцией веб-безопасности. Разработчики активно следят за новыми типами атак и адаптируют библиотеку соответственно. Это особенно важно, учитывая, как быстро меняется ландшафт угроз в вебе.

Morgan и Winston для логирования



Логирование — это тот аспект разработки, который обычно вспоминают только когда что-то идет не так. Но грамотно настроенное логирование может сэкономить часы отладки и буквально спасти ваш проект во время продакшен-кризиса. Здесь на помощь приходят два моих верных товарища: Morgan и Winston. Morgan специализируется на логировании HTTP-запросов и по сути является журналистом вашего приложения, записывающим каждое "событие" на границе вашего сервера. Его прелесть в простоте:

JavaScript
1
2
const morgan = require('morgan');
app.use(morgan('dev')); // Формат 'dev' для цветного вывода в консоль
Этой простой строчкой вы получаете информативные логи вида:
JavaScript
1
2
GET /api/users 200 54.721 ms - 1293
POST /api/auth 401 32.409 ms - 35
Такая информация незаменима при отладке и особено при расследовании инцедентов на продакшене. Я обычно настраиваю разные форматы для разных окружений:

JavaScript
1
2
3
4
5
if (process.env.NODE_ENV === 'production') {
  app.use(morgan('combined')); // Расширенный формат для продакшена
} else {
  app.use(morgan('dev')); // Компактный формат для разработки
}
Morgan предлагает несколько готовых форматов: combined, common, dev, short, tiny. Но реальная мощь проявляется когда вы создаете собственный формат под свои нужды:

JavaScript
1
app.use(morgan(':method :url :status :res[content-length] - :response-time ms - :remote-addr - :user-agent'));
Если вы хотите сохранять логи в файл вместо консоли, это тоже легко реализуется:

JavaScript
1
2
3
const fs = require('fs');
const accessLogStream = fs.createWriteStream(path.join(__dirname, 'access.log'), { flags: 'a' });
app.use(morgan('combined', { stream: accessLogStream }));
Но Morgan — это только начало истории логирования. Когда речь заходит о более сложных сценариях, на сцену выходит Winston — своего рода швейцарский нож логирования в мире Node.js.

В отличие от Morgan, который фокусируется на HTTP-запросах, Winston предназначен для логирования буквально всего в вашем приложении: от обычной отладочной информации до критических ошибок. Его главные преимущества — гибкость настройки и возможность отправлять логи в различные хранилища. Вот базовая настройка Winston:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const winston = require('winston');
 
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});
 
// В разработке также выводим в консоль
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}
С Winston можно делать гораздо больше чем просто записывать тексты в файл. Когда ваш проект растет, становится критически важным иметь структурированные логи, которые легко анализировать. Для этого я почти всегда использую формат JSON:

JavaScript
1
2
3
4
5
6
7
const logger = winston.createLogger({
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  // остальные настройки
});
Такой подход делает логи машиночитаемыми и позволяет их легко загружать в системы аналитики типа ELK (Elasticsearch, Logstash, Kibana) или Graylog. Причём, с современными инструментами вы можете отправлять логи напрямую из приложения:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
const { ElasticsearchTransport } = require('winston-elasticsearch');
 
const logger = winston.createLogger({
  transports: [
    new ElasticsearchTransport({
      level: 'info',
      index: 'app-logs',
      clientOpts: { node: 'http://localhost:9200' }
    })
  ]
});
Одна из самых распространеных ошибок — неправильная обработка ошибок в логах. Нередко вижу что-то вроде console.log('Error:', error), и это все. В реальном приложении такой подход приводит к потере ценной информации при диагностике проблем.

Вместо этого я обычно создаю специальный middleware для обработки ошибок, который не только логирует исключения, но и сохраняет контекст:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
// Обработчик ошибок для Express
app.use((err, req, res, next) => {
  logger.error({
    message: err.message,
    stack: err.stack,
    method: req.method,
    path: req.path,
    ip: req.ip,
    user: req.user ? req.user.id : 'anonymous'
  });
  
  res.status(500).json({ error: 'Что-то пошло не так' });
});
В одном проекте мы интегрировали систему алертинга Sentry с нашими логами. При критических ошибках команда получала уведомления в Slack почти мгновенно:

JavaScript
1
2
3
4
5
6
7
8
9
const Sentry = require('@sentry/node');
Sentry.init({ dsn: 'ваш-sentry-dsn' });
 
// Интеграция с Winston
const SentryTransport = require('winston-transport-sentry-node').default;
logger.add(new SentryTransport({
  sentry: Sentry,
  level: 'error'
}));
А для по-настоящему детального профилирования я иногда использую комбинацию Morgan и Winston. Morgan логирует базовую информацию о запросах, а Winston добавляет контекст бизнес-логики:

Compression middleware



Размер передаваемых данных напрямую влияет на скорость загрузки вашего веб-приложения. В эпоху, когда пользователи требуют мгновенной отдачи, каждый лишний килобайт может стоить вам клиента. Тут на помощь приходит compression — middleware, который автоматически сжимает ответы сервера. Это как вакуумный упаковщик для вашего контента — сжимает его перед отправкой, а браузер распаковывает при получении. Фантастика в том, что всё происходит совершенно прозрачно для клиента.

JavaScript
1
2
3
4
5
6
7
8
9
10
11
const compression = require('compression');
const express = require('express');
const app = express();
 
// Включаем сжатие для всех ответов
app.use(compression());
 
app.get('/api/data', (req, res) => {
  // Данные будут автоматически сжаты перед отправкой
  res.json(bigDataObject);
});
Эффект от такого простого действия может быть драматическим. Я как-то тестировал JSON-ответ размером 150KB, который после сжатия превратился в 12KB. Это больше чем 90% экономии! Особенно заметна разница на текстовом контенте — HTML, CSS, JavaScript, JSON.

Сжатие имеет огромное значение не только для экономии трафика, но и для улучшения воспринимаемой скорости загрузки страниц. Если вы когда-нибудь оптимизировали Google PageSpeed Insights или Lighthouse, то знаете, насколько критичен этот параметр. По умолчанию, compression использует алгоритм Gzip, который является неплохим компромисом между скоростью сжатия и степенью сжатия. Но иногда хочется большего контроля над процессом:

JavaScript
1
2
3
4
5
6
7
8
9
app.use(compression({
  level: 6,         // Уровень сжатия (0-9, по умолчанию 6)
  threshold: 1024,  // Минимальный размер для сжатия (в байтах)
  filter: (req, res) => {
    // Не сжимать уже сжатые форматы (изображения, видео и т.д.)
    const contentType = res.getHeader('Content-Type');
    return /text|json|javascript|css/.test(contentType);
  }
}));
Алгоритм сжатия работает не бесплатно — он потребляет процессорное время на сервере. Поэтому нужно искать баланс. Я обычно исключаю из сжатия файлы, которые уже сжаты (изображения, видео, PDF), потому что двойное сжатие часто не даёт преимуществ, а только тратит ресурсы.

Есть еще один момент — некоторые CDN и прокси (например, Nginx) могут выполнять сжатие самостоятельно. В таких случаях двойное сжатие в Node.js избыточно. Стоит проанализировать вашу инфраструктуру, прежде чем внедрять это решение. Впрочем, даже если вы используете CDN, compression middleware может пригодится для API-ответов или динамического контента, который не кешируется на уровне CDN.

В современных браузерах появилась поддержка более эффективного алгоритма сжатия — Brotli. По сравнению с Gzip, он обеспечивает лучшую степень сжатия при сопоставимой скорости декомпрессии. Реализовать его в Node.js можно с помощью middleware shrink-ray или compression-brotli:

JavaScript
1
2
const shrinkRay = require('shrink-ray-current');
app.use(shrinkRay());
Но ест нюанс — Brotli работает медленнее при сжатии, чем Gzip. На высоких уровнях сжатия разница становится существенной. На одном проекте я наблюдал, как сервер буквально захлебывался при попытке сжать большие JSON-ответы с помощью Brotli на максимальных настройках.

Хорошей практикой является использование кеширования сжатых ответов. Зачем сжимать один и тот же контент несколько раз? Я часто применяю такой подход:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const compression = require('compression');
const responseCache = new Map();
 
app.use((req, res, next) => {
  const originalSend = res.send;
  
  res.send = function(body) {
    const key = req.url;
    if (responseCache.has(key)) {
      res.setHeader('Content-Encoding', 'gzip');
      return originalSend.call(this, responseCache.get(key));
    }
    
    // Продолжаем нормальную обработку
    return originalSend.call(this, body);
  };
  
  next();
});
 
app.use(compression({
  level: 6,
  threshold: 1024,
  filter: (req, res) => {
    const key = req.url;
    // Кешируем сжатые ответы для будущих запросов
    const originalWrite = res.write;
    const chunks = [];
    
    res.write = function(chunk) {
      chunks.push(chunk);
      return originalWrite.apply(this, arguments);
    };
    
    const originalEnd = res.end;
    res.end = function(chunk) {
      if (chunk) chunks.push(chunk);
      responseCache.set(key, Buffer.concat(chunks));
      return originalEnd.apply(this, arguments);
    };
    
    return true;
  }
}));
Для больших объемов данных потоковая обработка становится критически важной. Node.js хорошо работает с потоками, и compression middleware автоматически использует эту возможность. Но можно пойти дальше и комбинировать сжатие с трансформацией данных:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const { createGzip } = require('zlib');
const { pipeline } = require('stream');
const fs = require('fs');
 
app.get('/large-file', (req, res) => {
  const fileStream = fs.createReadStream('path/to/large-file.json');
  const gzip = createGzip();
  
  res.setHeader('Content-Encoding', 'gzip');
  pipeline(fileStream, gzip, res, (err) => {
    if (err) {
      console.error('Pipeline failed', err);
    }
  });
});
Такой подход позволяет обрабатывать файлы практически любого размера без загрузки их целиком в память. Это особенно важно для микросервисов, которые часто работают с ограниченными ресурсами.

CORS и Passport



Когда дело доходит до взаимодействия клиента с сервером, возникают две фундаментальные проблемы: кто может обращаться к вашему API (CORS) и кто вообще этот пользователь (аутентификация). Эти вопросы настолько важны, что без их решения ваше приложение либо не заработает вообще, либо станет дырявым как швейцарский сыр.

Начнем с CORS (Cross-Origin Resource Sharing) — механизма, который контролирует, каким доменам разрешено взаимодействовать с вашим API. Браузеры по умолчанию блокируют запросы с одного домена к другому — это называется политикой одного источника (Same-Origin Policy). Представте, что вы разрабатываете фронтенд на React, который крутится на frontend.com и бэкенд на Node.js на api.com — без настройки CORS фронтенд просто не сможет получить данные с бэкенда. Middleware cors решает эту проблему:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
const cors = require('cors');
const app = express();
 
// Базовое использование - разрешает запросы со всех доменов
app.use(cors());
 
// Более ограниченная настройка
app.use(cors({
  origin: 'https://myapp.com', // разрешаем только этот домен
  methods: ['GET', 'POST'],    // разрешаем только эти методы
  allowedHeaders: ['Content-Type', 'Authorization'] // разрешаем эти заголовки
}));
На одном проекте я столкнулся с интересной ситуацией: нужно было разрешить запросы с нескольких доменов, но при этом сохранить безопасность. Вот как я это решил:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
const whitelist = ['https://app.mycompany.com', 'https://admin.mycompany.com'];
 
app.use(cors({
  origin: function(origin, callback) {
    // Проверяем, находится ли источник в белом списке
    if (whitelist.indexOf(origin) !== -1 || !origin) {
      callback(null, true);
    } else {
      callback(new Error('Доступ запрещен политикой CORS'));
    }
  },
  credentials: true // важно для передачи cookie и аутентификации
}));
Важный момент: не все запросы требуют CORS. Есть "простые запросы" (GET, HEAD, POST с определенными типами контента), которые не запускают preflight-запросы. Но как только вы добавляете заголовок Authorization или используете методы вроде PUT/DELETE, браузер сначала отправляет OPTIONS-запрос, чтобы убедиться, что такое взаимодействие разрешено.

Теперь перейдем к Passport — моему любимому middleware для аутентификации. Я называю его "швейцарским ножом аутентификации", потому что он поддерживает более 500 различных стратегий: от простого логин-пароля до OAuth с Google, Facebook, GitHub и так далее. Основная идея Passport — модульность. Вы подключаете только те стратегии, которые вам нужны:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
 
// Инициализация Passport
app.use(passport.initialize());
 
// Настройка локальной стратегии (логин/пароль)
passport.use(new LocalStrategy(
  function(username, password, done) {
    User.findOne({ username: username }, function(err, user) {
      if (err) { return done(err); }
      if (!user) { return done(null, false); }
      if (!user.verifyPassword(password)) { return done(null, false); }
      return done(null, user);
    });
  }
));
Но настройкой стратегии дело не ограничивается. Для полноценной работы аутентификации нужно реализовать маршруты для входа и защиты других эндпоинтов:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Маршрут для входа
app.post('/login', passport.authenticate('local', {
  successRedirect: '/dashboard',
  failureRedirect: '/login',
  failureFlash: true // если используете connect-flash
}));
 
// Защита маршрута
app.get('/profile', isAuthenticated, (req, res) => {
  res.json(req.user);
});
 
// Middleware для проверки аутентификации
function isAuthenticated(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  }
  res.redirect('/login');
}
В современных приложениях я все чаще отхожу от сессионной аутентификации в пользу JWT (JSON Web Tokens). JWT удобен тем, что не требует хранения состояния на сервере — вся информация о пользователе содержится в самом токене:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
 
const jwtOptions = {
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  secretOrKey: 'ваш-секретный-ключ'
};
 
passport.use(new JwtStrategy(jwtOptions, (jwtPayload, done) => {
  // Проверяем пользователя по данным из токена
  User.findById(jwtPayload.sub, (err, user) => {
    if (err) return done(err, false);
    if (user) return done(null, user);
    return done(null, false);
  });
}));
Для выдачи JWT я обычно использую отдельный маршрут:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const jwt = require('jsonwebtoken');
 
app.post('/api/login', (req, res) => {
  // Проверка учетных данных
  User.findOne({ username: req.body.username }, (err, user) => {
    if (err || !user || !user.verifyPassword(req.body.password)) {
      return res.status(401).json({ message: 'Неверные учетные данные' });
    }
    
    // Создаем JWT
    const token = jwt.sign(
      { sub: user._id, role: user.role },
      'ваш-секретный-ключ',
      { expiresIn: '1h' }
    );
    
    res.json({ token });
  });
});
В микросервисной архитектуре JWT особенно полезен, поскольку позволяет легко передавать аутентификационную информацию между сервисами. Когда пользователь входит в систему через сервис аутентификации, он получает JWT, который затем отправляется с запросами к другим микросервисам:

JavaScript
1
2
3
4
5
6
7
// Микросервис "Товары" проверяет JWT из запроса
app.get('/api/products', passport.authenticate('jwt', { session: false }), (req, res) => {
  // JWT прошел проверку, пользователь доступен в req.user
  Product.find({ userId: req.user._id }, (err, products) => {
    res.json(products);
  });
});
Один интересный паттерн, который я часто использую в микросервисных архитектурах — это "межсервисная аутентификация". Суть в том, что сервисы общаются не только по запросу клиента, но и напрямую друг с другом. В таких случаях JWT используется не от имени пользователя, а от имени самого сервиса:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getServiceToken() {
  return jwt.sign(
    { service: 'order-service', permissions: ['read:products', 'write:orders'] },
    process.env.SERVICE_SECRET,
    { expiresIn: '1h' }
  );
}
 
// Вызов микросервиса "Товары" из микросервиса "Заказы"
axios.get('http://product-service/api/products/inventory', {
  headers: {
    Authorization: [INLINE]Bearer ${getServiceToken()}[/INLINE]
  }
})
.then(response => processInventory(response.data))
.catch(error => handleError(error));
Важный момент безопасности — хранение и обновление токенов. JWT имеет ограниченый срок жизни (обычно от нескольких минут до нескольких часов). Как обновлять токен, не заставляя пользователя постоянно входить заново? Для этого я использую механизм refresh-токенов:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
app.post('/api/token/refresh', (req, res) => {
  const { refreshToken } = req.body;
  
  if (!refreshToken) return res.status(401).json({ message: 'Refresh токен не предоставлен' });
  
  // Проверяем refresh токен в базе данных
  RefreshToken.findOne({ token: refreshToken }, (err, token) => {
    if (err || !token) return res.status(401).json({ message: 'Недействительный токен' });
    
    // Проверяем не отозван ли токен
    if (token.revoked) return res.status(401).json({ message: 'Токен отозван' });
    
    // Создаем новый access токен
    const user = token.user;
    const newAccessToken = jwt.sign(
      { sub: user._id, role: user.role },
      process.env.JWT_SECRET,
      { expiresIn: '1h' }
    );
    
    res.json({ token: newAccessToken });
  });
});
Еще один важный аспект — отзыв токенов. В отличие от сессий, JWT нельзя просто "уничтожить" на сервере, поскольку они автономны. Поэтому нужно вести "черный список" отозваных токенов:

Rate limiting и валидация данных



Когда ваше API становится популярным, это палка о двух концах. С одной стороны, растёт аудитория и бизнес, с другой — появляются боты, скрейперы и просто недружелюбные пользователи, которые могут положить ваш сервер парой строк кода. Я дважды сталкивался с ситуациями, когда новый клиент присылал тысячи запросов в секунду из-за ошибки в своём коде, и сервер просто сдавался.

Тут на сцену выходит rate limiting — ограничение количества запросов с одного IP-адреса за определённый промежуток времени. В Express это делается элементарно с помощью middleware express-rate-limit:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const rateLimit = require('express-rate-limit');
 
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 минут
  max: 100, // 100 запросов с одного IP
  message: 'Слишком много запросов с вашего IP, попробуйте позже'
});
 
// Применяем к API маршрутам
app.use('/api/', apiLimiter);
 
// Отдельный лимитер для авторизации (защита от брутфорса)
const loginLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 час
  max: 5, // 5 попыток в час
  message: 'Слишком много неудачных попыток входа, попробуйте позже'
});
 
app.post('/login', loginLimiter, authController.login);
В сложных приложениях я часто использую разные лимиты для разных эндпоинтов. Представте, что у вас есть тяжелая операция поиска по базе — её стоит ограничить сильнее, чем простое получение статического контента.
Для распределенных систем простого лимитера с хранением в памяти недостаточно. Если у вас несколько инстансов приложения за балансировщиком, счетчики запросов нужно хранить в общем хранилище, например, Redis:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const redis = require('redis');
const { RateLimiterRedis } = require('rate-limiter-flexible');
 
const redisClient = redis.createClient({
  host: process.env.REDIS_HOST,
  port: process.env.REDIS_PORT,
  enable_offline_queue: false
});
 
const rateLimiter = new RateLimiterRedis({
  storeClient: redisClient,
  keyPrefix: 'middleware',
  points: 10, // 10 запросов
  duration: 1, // за 1 секунду
});
 
app.use(async (req, res, next) => {
  try {
    await rateLimiter.consume(req.ip);
    next();
  } catch (err) {
    res.status(429).send('Слишком много запросов');
  }
});
Но ограничивать количество запросов — только половина дела. Не менее важно проверять, что именно приходит в этих запросах. Валидация входящих данных — это иммунная система вашего API.

Для валидации входящих данных в Node.js есть два основных бойца: Joi и express-validator. Лично я долго использовал Joi из-за его декларативного синтаксиса, но в последнее время перешел на express-validator — он лучше интегрируется с Express и проще для новичков в команде. Вот простой пример валидации с express-validator:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const { body, validationResult } = require('express-validator');
 
app.post('/user', [
  body('email').isEmail().normalizeEmail(),
  body('password').isLength({ min: 8 }).withMessage('Пароль должен содержать минимум 8 символов'),
  body('name').not().isEmpty().trim().escape()
], (req, res) => {
  // Проверяем результаты валидации
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  
  // Если всё ок, продолжаем
  createUser(req.body);
  res.status(201).send('Пользователь создан');
});
Такой подход дает несколько преимуществ: предотвращает SQL-инъекции (если вы используете ORM с параметризованными запросами), защищает от XSS-атак через escape() и нормализует данные через trim() и normalizeEmail(). Для сложных схем валидации, особенно при работе с вложенными объектами, я предпочитаю Joi:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const Joi = require('joi');
 
const orderSchema = Joi.object({
  customer: Joi.object({
    name: Joi.string().required(),
    email: Joi.string().email().required(),
    address: Joi.string().min(10)
  }),
  items: Joi.array().items(
    Joi.object({
      productId: Joi.string().required(),
      quantity: Joi.number().integer().min(1).required(),
      price: Joi.number().precision(2).positive()
    })
  ).min(1).required(),
  payment: Joi.object({
    method: Joi.string().valid('card', 'paypal', 'cash').required(),
    // Условная валидация - если метод card, то нужны доп. поля
    cardNumber: Joi.when('method', {
      is: 'card',
      then: Joi.string().required(),
      otherwise: Joi.forbidden()
    })
  })
});
 
app.post('/order', (req, res) => {
  const { error, value } = orderSchema.validate(req.body);
  
  if (error) {
    return res.status(400).json({ error: error.details[0].message });
  }
  
  // Данные прошли валидацию и доступны в value
  processOrder(value);
  res.status(201).send('Заказ принят');
});

Multer для файлов и body-parser



Загрузка файлов и парсинг данных формы — две задачи, которые вызывают головную боль у многих разработчиков. Помню, как в начале своей карьеры я написал собственный обработчик для мультипарт-данных... Тот монстр развалился при первой же серьезной нагрузке. Сейчас эти проблемы решаются элегантно с помощью специализированных middleware.

Multer — настоящий спаситель когда дело касается загрузки файлов. Он специально создан для обработки multipart/form-data, что делает его идеальным для файловых операций:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const multer = require('multer');
 
// Настраиваем хранилище
const storage = multer.diskStorage({
  destination: function(req, file, cb) {
    cb(null, './uploads/'); // папка для сохранения
  },
  filename: function(req, file, cb) {
    // создаем уникальное имя файла
    cb(null, Date.now() + '-' + file.originalname);
  }
});
 
// Настраиваем фильтр файлов
const fileFilter = (req, file, cb) => {
  // Принимаем только изображения
  if (file.mimetype.startsWith('image/')) {
    cb(null, true);
  } else {
    cb(new Error('Недопустимый тип файла!'), false);
  }
};
 
const upload = multer({ 
  storage: storage,
  limits: {
    fileSize: 1024 * 1024 * 5 // 5MB макс размер
  },
  fileFilter: fileFilter
});
 
// Загрузка одного файла
app.post('/upload', upload.single('profileImage'), (req, res) => {
  // req.file содержит информацию о загруженном файле
  // req.body содержит текстовые поля формы
  res.json({
    path: req.file.path,
    message: 'Файл успешно загружен'
  });
});
 
// Загрузка нескольких файлов
app.post('/upload-multiple', upload.array('gallery', 10), (req, res) => {
  res.json({
    files: req.files,
    message: [INLINE]Загружено ${req.files.length} файлов[/INLINE]
  });
});
Что касается body-parser, ситуация интересная. Раньше это был отдельный пакет, но с Express 4.16+ он встроен в сам фреймворк. Он парсит тела запросов и делает данные доступными в req.body:

JavaScript
1
2
3
4
5
6
7
8
// Старый способ (до Express 4.16)
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
 
// Новый способ (Express 4.16+)
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
Параметр extended: true позволяет парсить вложенные объекты и массивы, что критично для сложных форм. Я однажды потратил часы на отладку, потому что забыл установить этот флаг при обработке сложной анкеты.
Для больших файлов стандартная конфигурация Multer может быть проблематичной. Когда пользователь загружает видео на 2GB, вы не хотите, чтобы оно целиком помещалось в память. Тут на помощь приходит потоковая обработка:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const storage = multer.memoryStorage();
const upload = multer({ storage });
 
app.post('/upload-video', upload.single('video'), (req, res) => {
  const fileBuffer = req.file.buffer;
  
  // Создаем поток для записи
  const writeStream = fs.createWriteStream(`./uploads/${req.file.originalname}`);
  
  // Преобразуем буфер в поток и пишем
  const readStream = new Readable();
  readStream.push(fileBuffer);
  readStream.push(null);
  
  readStream.pipe(writeStream);
  
  writeStream.on('finish', () => {
    res.json({ message: 'Видео успешно загружено' });
  });
});
Но есть решение еще лучше — вообще избегать буферизации с помощью multer-s3 или похожих модулей:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const aws = require('aws-sdk');
const multerS3 = require('multer-s3');
 
const s3 = new aws.S3({ /* настройки */ });
 
const upload = multer({
  storage: multerS3({
    s3: s3,
    bucket: 'my-bucket',
    contentType: multerS3.AUTO_CONTENT_TYPE,
    key: function (req, file, cb) {
      cb(null, Date.now().toString() + '-' + file.originalname);
    }
  })
});
 
app.post('/upload', upload.single('file'), (req, res) => {
  res.json({ url: req.file.location });
});
Для действительно громадных файлов я использую подход с разбиением на части. Клиент разбивает файл на чанки и отправляет их последовательно:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
app.post('/upload-chunk', (req, res) => {
  const { index, total, fileId } = req.query;
  const chunkFolder = path.join(__dirname, 'chunks', fileId);
  
  // Создаем папку для чанков, если её нет
  if (!fs.existsSync(chunkFolder)) {
    fs.mkdirSync(chunkFolder, { recursive: true });
  }
  
  // Сохраняем чанк
  const writeStream = fs.createWriteStream(path.join(chunkFolder, index));
  req.pipe(writeStream);
  
  writeStream.on('finish', () => {
    // Проверяем, все ли чанки получены
    if (fs.readdirSync(chunkFolder).length == total) {
      // Собираем файл из чанков
      combineChunks(fileId);
    }
    res.json({ status: 'ok' });
  });
});
Что касается валидации файлов, помимо проверки MIME-типа, часто требуется более глубокий анализ. Например, проверка метаданных изображения с помощью модуля sharp:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const sharp = require('sharp');
 
app.post('/upload-image', upload.single('image'), async (req, res) => {
  try {
    // Проверяем, что это реально изображение
    const metadata = await sharp(req.file.path).metadata();
    
    // Проверяем размеры
    if (metadata.width > 5000 || metadata.height > 5000) {
      fs.unlinkSync(req.file.path); // Удаляем файл
      return res.status(400).send('Изображение слишком большое');
    }
    
    // Можно даже изменить изображение на лету
    await sharp(req.file.path)
      .resize(800, 600)
      .toFile(req.file.path + '_resized.jpg');
    
    res.json({ success: true });
  } catch (error) {
    // Если произошла ошибка - это не изображение
    fs.unlinkSync(req.file.path);
    res.status(400).send('Недопустимый формат файла');
  }
});
В одном проекте я столкнулся с необходимостью обрабатывать и JSON, и FormData в одном API. Решение оказалось неочевидным:

Middleware для профилирования и метрик APM в production среде



APM (Application Performance Monitoring) middleware — это ваши глаза и уши в production-среде. Они позволяют собирать метрики производительности, отслеживать задержки и находить узкие места без воздействия на работу пользователей.

Один из моих любимых инструментов — express-prometheus-middleware, который позволяет собирать метрики для Prometheus:

JavaScript
1
2
3
4
5
6
7
8
9
const prometheus = require('express-prometheus-middleware');
 
app.use(prometheus({
  metricsPath: '/metrics',
  collectDefaultMetrics: true,
  requestDurationBuckets: [0.1, 0.5, 1, 1.5, 2, 5],
  requestLengthBuckets: [512, 1024, 5120, 10240, 51200, 102400],
  responseLengthBuckets: [512, 1024, 5120, 10240, 51200, 102400],
}));
Этот код добавляет эндпоинт /metrics, который возвращает метрики в формате, который понимает Prometheus. Теперь вы можете видеть распределение времени обработки запросов, количество запросов по статус-кодам и многое другое.

Для распределенного трейсинга я часто использую OpenTelemetry — открытый стандарт для сбора трейсов и метрик:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
const opentelemetry = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { diag, DiagConsoleLogger, DiagLogLevel } = require('@opentelemetry/api');
 
// Включаем логирование для отладки
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
 
const sdk = new opentelemetry.NodeSDK({
  traceExporter: new opentelemetry.tracing.ConsoleSpanExporter(),
  instrumentations: [getNodeAutoInstrumentations()]
});
 
sdk.start();
Тут фишка в том, что OpenTelemetry автоматически инструментирует популярные библиотеки, включая Express, MongoDB, Redis и другие, без необходимости менять ваш код.
Для более детального профилирования я создаю middleware, которые измеряют время выполнения отдельных операций:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function profileMiddleware(operationName) {
  return (req, res, next) => {
    const start = process.hrtime();
    
    // Перехватываем метод end для измерения времени
    const originalEnd = res.end;
    res.end = function(...args) {
      const diff = process.hrtime(start);
      const time = diff[0] * 1e3 + diff[1] * 1e-6; // в миллисекундах
      
      // Отправляем метрику в систему мониторинга
      metrics.timing(`operation.${operationName}`, time);
      
      return originalEnd.apply(this, args);
    };
    
    next();
  };
}
 
// Применение
app.get('/api/products', 
  profileMiddleware('fetchProducts'), 
  productsController.getAll
);
Для бизнес-метрик, которые выходят за рамки технических показателей, я использую специальные middleware. Например, мониторинг конверсии в воронке продаж:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function businessMetricsMiddleware() {
  return (req, res, next) => {
    // Сохраняем оригинальный метод json
    const originalJson = res.json;
    
    res.json = function(body) {
      // Собираем бизнес-метрики на основе ответа
      if (body.orderCompleted) {
        metrics.increment('sales.completed');
        metrics.gauge('sales.amount', body.orderTotal);
      }
      
      return originalJson.call(this, body);
    };
    
    next();
  };
}
Когда дело доходит до визуализации собраных данных, я обычно интегрирую Grafana с Prometheus. Это дает красивые дашборды, которые понятны не только разработчикам, но и менеджерам.

В особо нагруженных системах я добавляю профилирование памяти. Утечки памяти в Node.js — настоящая головная боль, и без правильных инструментов их сложно диагностировать:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const memwatch = require('@airbnb/node-memwatch');
 
app.use((req, res, next) => {
  // Отслеживаем утечки памяти
  const hd = new memwatch.HeapDiff();
  
  res.on('finish', () => {
    const diff = hd.end();
    if (diff.change.size_bytes > 10000000) { // 10MB утечка
      console.warn('Возможная утечка памяти:', diff);
      metrics.gauge('memory.leak.size', diff.change.size_bytes);
    }
  });
  
  next();
});
Для кеширования я часто использую Redis в комбинации с middleware. Это существенно снижает нагрузку на базу данных:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const redis = require('redis');
const client = redis.createClient();
 
function cacheMiddleware(duration) {
  return (req, res, next) => {
    const key = `cache:${req.originalUrl}`;
    
    client.get(key, (err, cachedResponse) => {
      if (cachedResponse) {
        return res.send(JSON.parse(cachedResponse));
      }
      
      // Перехватываем метод send для кеширования ответа
      const originalSend = res.send;
      res.send = function(body) {
        client.setex(key, duration, JSON.stringify(body));
        return originalSend.call(this, body);
      };
      
      next();
    });
  };
}

Нестандартные решения и собственные middleware



Один из самых полезных middleware, который я написал — динамический транслейтер URL. В многоязычном приложении нам требовалось поддерживать разные форматы URL для разных языков. Вместо того чтобы хардкодить маршруты для каждого языка, я создал middleware, который преобразовывал URL на лету:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function urlTranslator(urlMappings) {
  return (req, res, next) => {
    const userLang = req.headers['accept-language']?.split(',')[0] || 'en';
    const currentPath = req.path;
    
    // Проверяем, есть ли маппинг для текущего пути и языка
    if (urlMappings[currentPath] && urlMappings[currentPath][userLang]) {
      // Меняем путь внутри приложения, но сохраняем оригинальный URL
      req.originalPath = currentPath;
      req.url = req.url.replace(currentPath, urlMappings[currentPath][userLang]);
    }
    
    next();
  };
}
 
// Использование
app.use(urlTranslator({
  '/about': {
    'ru': '/о-нас',
    'de': '/uber-uns'
  },
  '/products': {
    'ru': '/товары',
    'de': '/produkte'
  }
}));
Другая нестандартная задача, с которой я сталкнулся — автоматическое определение версии API на основе User-Agent клиента. Старые мобильные приложения должны были работать со старой версией API, новые — с новой:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function apiVersionResolver() {
  return (req, res, next) => {
    const userAgent = req.get('User-Agent') || '';
    let apiVersion = 'v1'; // По умолчанию
    
    // Определяем версию по User-Agent
    if (userAgent.includes('MyApp/2.')) {
      apiVersion = 'v2';
    } else if (userAgent.includes('MyApp/3.')) {
      apiVersion = 'v3';
    }
    
    // Изменяем путь запроса, добавляя версию
    if (!req.path.includes(`/api/${apiVersion}/`)) {
      req.url = req.url.replace('/api/', [INLINE]/api/${apiVersion}/[/INLINE]);
    }
    
    req.apiVersion = apiVersion; // Сохраняем для использования в обработчиках
    next();
  };
}
Иногда возникают совсем экзотические задачи. В одном проекте мне пришлось создать middleware для динамической генерации скриншотов страниц с помощью Puppeteer:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const puppeteer = require('puppeteer');
 
function screenshotMiddleware() {
  let browser;
  
  // Инициализация браузера при старте сервера
  (async () => {
    browser = await puppeteer.launch({ headless: true });
  })();
  
  return async (req, res, next) => {
    // Проверяем, нужен ли скриншот (например, по query-параметру)
    if (!req.query.screenshot) return next();
    
    try {
      const page = await browser.newPage();
      // Формируем полный URL страницы
      const pageUrl = `${req.protocol}://${req.get('host')}${req.path}`;
      
      await page.goto(pageUrl, { waitUntil: 'networkidle2' });
      const screenshot = await page.screenshot({ fullPage: true });
      
      await page.close();
      
      res.type('image/png').send(screenshot);
    } catch (err) {
      next(err);
    }
  };
}
Еще один полезный middleware, который я разработал для высоконагруженого проекта, — динамический шейпер ответов, позволяющий клиенту запрашивать только те поля, которые ему нужны:

Тестирование Express приложения с использованием всех middleware



Тестирование Express-приложения с десятком middleware — задача не для слабонервных. Раньше я игнорировал эту часть разработки, пока однажды не накосячил с middleware-цепочкой в продакшене, и пользователи начали получать доступ к чужим данным. С тех пор я тестирую все аспекты своих приложений.

Для тестирования отдельных middleware в изоляции я использую библиотеку node-mocks-http, которая эмулирует объекты запроса и ответа Express:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const httpMocks = require('node-mocks-http');
const authMiddleware = require('../middleware/auth');
 
test('auth middleware должен вернуть 401 без токена', () => {
  // Создаем мок-объекты
  const req = httpMocks.createRequest();
  const res = httpMocks.createResponse();
  const next = jest.fn();
  
  // Вызываем тестируемый middleware
  authMiddleware(req, res, next);
  
  // Проверяем результаты
  expect(res.statusCode).toBe(401);
  expect(next).not.toHaveBeenCalled();
});
Но тестирование отдельных компонентов не гарантирует работу системы в целом. Для интеграционного тестирования я создаю реальное Express-приложение и отправляю запросы с помощью supertest:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const request = require('supertest');
const app = require('../app');
 
describe('Интеграционные тесты API', () => {
  test('GET /api/users должен вернуть 401 без авторизации', async () => {
    const response = await request(app).get('/api/users');
    expect(response.statusCode).toBe(401);
  });
  
  test('POST /api/users должен проверять валидацию', async () => {
    const response = await request(app)
      .post('/api/users')
      .set('Authorization', [INLINE]Bearer ${testToken}[/INLINE])
      .send({ name: '' }); // Невалидные данные
    
    expect(response.statusCode).toBe(400);
    expect(response.body).toHaveProperty('errors');
  });
});
Интеграция тестов с CI/CD — отдельная песня. Я подключаю Jest к GitHub Actions для автоматического запуска тестов при каждом пуше:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
name: Node.js Tests
 
on: [push, pull_request]
 
jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js
      uses: actions/setup-node@v1
      with:
        node-version: '16.x'
    - run: npm ci
    - run: npm test
Особенно сложно тестировать middleware, которые зависят от внешних сервисов. Тут на помощь приходит мокирование. Я обычно создаю мок-файлы в папке __mocks__:

JavaScript
1
2
3
4
5
6
7
8
9
// __mocks__/redis.js
module.exports = {
  createClient: jest.fn().mockReturnValue({
    get: jest.fn().mockImplementation((key, callback) => {
      callback(null, null); // Симулируем отсутствие данных в кеше
    }),
    setex: jest.fn()
  })
};
Для тестирования безопасности я пишу специальные тесты, проверяющие работу helmet и других защитных middleware:

Заключение



В конце концов, эффективная разработка на Node.js сводится к правильному выбору инструментов и их грамотной комбинации. Хорошие middleware — как надежные кирпичики, из которых строится крепкое здание вашего приложения.
Вот пример, объединяющий основные middleware из нашего обзора в одном Express-приложении:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
const express = require('express');
const helmet = require('helmet');
const morgan = require('morgan');
const compression = require('compression');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const { body, validationResult } = require('express-validator');
const multer = require('multer');
const passport = require('passport');
const JwtStrategy = require('passport-jwt').Strategy;
 
const app = express();
 
// 1. Инфраструктурные middleware
app.use(morgan('combined')); // Логирование
app.use(express.json()); // Парсинг JSON
app.use(express.urlencoded({ extended: true })); // Парсинг URL-encoded
app.use(compression()); // Сжатие ответов
 
// 2. Безопасность
app.use(helmet()); // Заголовки безопасности
app.use(cors({
  origin: ['https://myapp.com', 'https://admin.myapp.com'],
  credentials: true
}));
app.use(rateLimit({
  windowMs: 15 * 60 * 1000, // 15 минут
  max: 100 // 100 запросов с одного IP
}));
 
// 3. Аутентификация
app.use(passport.initialize());
// Настройка JWT-стратегии...
 
// 4. Маршруты с валидацией и обработкой файлов
const upload = multer({ dest: 'uploads/' });
 
app.post('/user', [
  body('email').isEmail(),
  body('password').isLength({ min: 8 })
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
  // Создание пользователя...
});
 
app.post('/upload', upload.single('file'), (req, res) => {
  // Обработка загрузки файла...
});
 
// 5. Обработка ошибок (всегда в конце)
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Что-то пошло не так!');
});
 
app.listen(3000, () => console.log('Сервер запущен на порту 3000'));
Такая структура обеспечивает хороший баланс между безопасностью, производительностью и удобством разработки. Меняйте и комбинируйте middleware исходя из своих специфических задач — в этом и заключается искусство разработки на Node.js.

Как сделать дополнительный запрос в middleware
Здравствуйте Мне нужно в мидлвэре найти нужный мне запрос, где есть параметр page. У этого...

React middleware error
тут проект с ошибкой - https://github.com/SashaMaksyutenko/payShopReact Подскажите пожалуйста как...

Uncaught TypeError: Failed to execute 'removeChild' on 'Node': parameter 1 is not of type 'Node'
Привет, есть следующий код который срабатывает правильно, как и задумано (когда создано...

Не запускается пакет node js - пакетами? npm? сам node? gulp?
Всем доброго времени суток. Есть такая проблема, пытаюсь перебраться на Linux (Ubuntu) Установил...

Выложил приложение Node js на хост, ошибка (node:12900) [DEP0005] DeprecationWarning: Buffer()
Выложил приложение Node js на хост, ошибка (node:12900) DeprecationWarning: Buffer() is deprecated...

Не могу с решениями задач на node js (я понимаю как их решить на js, но как на node js не знаю)
1) Однажды ковбой Джо решил обзавестись револьвером и пришёл в оружейный магазин. У ковбоя s...

Алгоритм эффективного перебора чисел
Найти максимальный палиндром. Каким образом организовать алгоритм и перебор элементов, чтобы ...

С клавиатуры вводятся десять натуральных чисел
С клавиатуры вводятся десять натуральных чисел. Сформировать с помощью кортежа массив, в котором...

Как создать директорию для нового проекта в Node js?
Подскажите, как сделать, чтоб Node js находила отдельную папку с файлами. Ищет файлы только в...

Посоветуйте книгу для начинающего node.js
добрый день Посоветуйте книгу для начинающего node.js Если я гуру в angularjs Спасибо

Скрипт бот для Steam NODE.JS
Вот такую ошибку выводит скрипт при его запуске. Сначала открывается командая строка, появляется...

Node.js Tools для Visual Studio
Скачал Node Js tools для visual studio, но не могу разобраться с одной вещью. В WebStorm'е был...

Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Запрет удаления строк ТЧ документа при определенном условии
Maks 19.04.2026
Алгоритм из решения ниже реализован на примере нетипового документа "Аккумуляторы", разработанного в конфигурации КА2. У данного документа есть ТЧ, в которой в зависимости от прав доступа. . .
Модель заражения группы наркоманов
alhaos 17.04.2026
Условия задачи сформулированы тут Суть: - Группа наркоманов из 10 человек. - Только один инфицирован ВИЧ. - Колются одной иглой. - Колются раз в день. - Колются последовательно через. . .
Мысли в слух. Про "навсегда".
kumehtar 16.04.2026
Подумалось тут, что наверное очень глупо использовать во всяких своих установках понятие "навсегда". Это очень сильное понятие, и я только начинаю понимать край его смысла, не смотря на то что давно. . .
My Business CRM
MaGz GoLd 16.04.2026
Всем привет, недавно возникла потребность создать CRM, для личных нужд. Собственно программа предоставляет из себя базу данных клиентов, в которой можно фиксировать звонки, стадии сделки, а также. . .
Знаешь почему 90% людей редко бывают счастливыми?
kumehtar 14.04.2026
Потому что они ждут. Ждут выходных, ждут отпуска, ждут удачного момента. . . а удачный момент так и не приходит.
Фиксация колонок в отчете СКД
Maks 14.04.2026
Фиксация колонок в СКД отчета типа Таблица. Задача: зафиксировать три левых колонки в отчете. Процедура ПриКомпоновкеРезультата(ДокументРезультат, ДанныеРасшифровки, СтандартнаяОбработка) / / . . .
Настройки VS Code
Loafer 13.04.2026
{ "cmake. configureOnOpen": false, "diffEditor. ignoreTrimWhitespace": true, "editor. guides. bracketPairs": "active", "extensions. ignoreRecommendations": true, . . .
Оптимизация кода на разграничение прав доступа к элементам формы
Maks 13.04.2026
Алгоритм из решения ниже реализован на нетиповом документе, разработанного в конфигурации КА2. Задачи, как таковой, поставлено не было, проделанное ниже исключительно моя инициатива. Было так:. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru