Тема 9. Валідація даних та конвеєр обробки запитів у NestJS

Практичні приклади Middleware

Логування запитів, вимірювання часу, CORS, аутентифікація

Практичні приклади Middleware

🎯 Мета лекції

  • Опанувати створення практичних middleware для типових задач веб-розробки
  • Навчитися реалізовувати LoggerMiddleware для структурованого логування HTTP-запитів
  • Вивчити TimingMiddleware для вимірювання продуктивності обробки запитів
  • Засвоїти механізми автентифікації через JwtAuthMiddleware з перевіркою токенів
  • Розуміти налаштування CORS через CorsMiddleware для міжсайтових запитів
  • Практикувати інтеграцію сторонніх middleware: compression, helmet, rate limiting
  • Навчитися створювати RequestIdMiddleware для трейсингу запитів у розподілених системах

🔑 Ключові терміни

  • Structured Logging (структуроване логування): запис логів у форматі JSON з метаданими для автоматизованого аналізу
  • Request Tracing (трейсинг запитів): відстеження шляху запиту через мікросервіси за унікальним ідентифікатором
  • Response Time Measurement (вимірювання часу відповіді): метрика продуктивності для моніторингу API
  • JWT Authentication (автентифікація JWT): механізм перевірки токенів у заголовку Authorization
  • CORS (Cross-Origin Resource Sharing) (міжсайтовий обмін ресурсами): механізм HTTP-заголовків для дозволу запитів з інших доменів
  • Rate Limiting (обмеження частоти запитів): захист API від зловживань через ліміт запитів на IP-адресу
  • Content Security Policy (політика безпеки вмісту): набір HTTP-заголовків для захисту від XSS та ін'єкцій

LoggerMiddleware: структуроване логування запитів

Логування HTTP-запитів є фундаментальною вимогою будь-якого production-застосунку. Структуровані логи дозволяють швидко діагностувати проблеми, аналізувати трафік та виявляти аномалії через інструменти агрегації логів (Elasticsearch, Datadog, Grafana Loki).

Базовий LoggerMiddleware

Почнемо з простого middleware для виведення деталей запиту в консоль:

import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  private readonly logger = new Logger('HTTP');

  use(req: Request, res: Response, next: NextFunction) {
    const { method, originalUrl, ip } = req;
    const userAgent = req.get('user-agent') || '';

    // Логування початку обробки запиту
    this.logger.log(`[${method}] ${originalUrl} - IP: ${ip}`);

    next();
  }
}

Консольний вивід:

npm run start:dev
[Nest] 12345 - 09/05/2026, 10:30:00 AM LOG [HTTP] [GET] /api/users - IP: ::1
[Nest] 12345 - 09/05/2026, 10:30:01 AM LOG [HTTP] [POST] /api/auth/login - IP: 192.168.1.100
[Nest] 12345 - 09/05/2026, 10:30:02 AM LOG [HTTP] [DELETE] /api/users/42 - IP: ::ffff:127.0.0.1

Розширений LoggerMiddleware з часом відповіді

Для вимірювання часу виконання запиту використовуємо подію res.on('finish'):

import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class EnhancedLoggerMiddleware implements NestMiddleware {
  private readonly logger = new Logger('HTTP');

  use(req: Request, res: Response, next: NextFunction) {
    const { method, originalUrl, ip } = req;
    const userAgent = req.get('user-agent') || '';
    const startTime = Date.now();

    // Логування після завершення відповіді
    res.on('finish', () => {
      const { statusCode } = res;
      const duration = Date.now() - startTime;
      
      const logMessage = `${method} ${originalUrl} ${statusCode} - ${duration}ms`;
      
      // Колір логу залежить від статус-коду
      if (statusCode >= 500) {
        this.logger.error(logMessage);
      } else if (statusCode >= 400) {
        this.logger.warn(logMessage);
      } else {
        this.logger.log(logMessage);
      }
    });

    next();
  }
}

Консольний вивід з кольоровою індикацією:

npm run start:dev
[Nest] 12345 - 09/05/2026, 10:30:00 AM LOG [HTTP] GET /api/users 200 - 45ms
[Nest] 12345 - 09/05/2026, 10:30:01 AM WARN [HTTP] POST /api/users 400 - 12ms
[Nest] 12345 - 09/05/2026, 10:30:02 AM ERROR [HTTP] DELETE /api/users/999 500 - 203ms
Вимірювання часу через подію finish є точнішим за вимірювання через код після next(), оскільки finish емітується після відправлення останнього байта відповіді клієнту, включаючи час серіалізації JSON та стиснення (якщо увімкнено compression middleware).

Структурований JSON-логер для production

Для production-середовищ краще використовувати JSON-формат логів, що дозволяє агрегувати та аналізувати їх через сторонні сервіси:

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

interface LogEntry {
  timestamp: string;
  method: string;
  url: string;
  statusCode: number;
  duration: number;
  ip: string;
  userAgent: string;
  requestId?: string;
}

@Injectable()
export class JsonLoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const startTime = Date.now();

    res.on('finish', () => {
      const logEntry: LogEntry = {
        timestamp: new Date().toISOString(),
        method: req.method,
        url: req.originalUrl,
        statusCode: res.statusCode,
        duration: Date.now() - startTime,
        ip: req.ip,
        userAgent: req.get('user-agent') || 'unknown',
        requestId: req['requestId'], // Якщо використовується RequestIdMiddleware
      };

      // Виведення у форматі JSON (одна строка на запис)
      console.log(JSON.stringify(logEntry));
    });

    next();
  }
}

Приклад виводу (кожен рядок — окремий JSON-об'єкт):

{"timestamp":"2026-09-05T10:30:00.123Z","method":"GET","url":"/api/users","statusCode":200,"duration":45,"ip":"::1","userAgent":"Mozilla/5.0","requestId":"req-1725536400123"}
{"timestamp":"2026-09-05T10:30:01.456Z","method":"POST","url":"/api/auth/login","statusCode":201,"duration":89,"ip":"192.168.1.100","userAgent":"curl/7.88.1","requestId":"req-1725536401456"}
JSON-логи можна направити в стандартний вивід (stdout) і підключити до агрегаторів логів:
  • Elasticsearch + Kibana: через Filebeat або Logstash
  • Grafana Loki: через Promtail
  • Datadog: через Datadog Agent
  • AWS CloudWatch: через CloudWatch Logs Agent
Це дозволяє створювати дашборди, налаштовувати алерти та аналізувати аномалії в реальному часі.

RequestIdMiddleware: трейсинг запитів

У розподілених системах з мікросервісами критично важливо відстежувати запит через весь ланцюг сервісів. RequestIdMiddleware генерує унікальний ідентифікатор для кожного запиту та додає його до всіх логів.

Генерація унікального ID

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { v4 as uuidv4 } from 'uuid';

@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    // Перевірка наявності X-Request-ID з upstream-сервісу
    const existingId = req.headers['x-request-id'] as string;
    const requestId = existingId || uuidv4();

    // Прикріплення до request для використання в логах
    req['requestId'] = requestId;

    // Додавання до заголовків відповіді для клієнта
    res.setHeader('X-Request-ID', requestId);

    next();
  }
}

Встановлення UUID:

npm install uuid
npm install -D @types/uuid

Інтеграція з LoggerMiddleware

Комбінація RequestIdMiddleware та LoggerMiddleware дозволяє трейсити всі запити:

@Injectable()
export class TracingLoggerMiddleware implements NestMiddleware {
  private readonly logger = new Logger('HTTP');

  use(req: Request, res: Response, next: NextFunction) {
    const { method, originalUrl } = req;
    const requestId = req['requestId'];
    const startTime = Date.now();

    res.on('finish', () => {
      const { statusCode } = res;
      const duration = Date.now() - startTime;
      
      this.logger.log(
        `[${requestId}] ${method} ${originalUrl} ${statusCode} - ${duration}ms`
      );
    });

    next();
  }
}

Порядок реєстрації middleware (requestId має бути першим):

@Module({
  providers: [RequestIdMiddleware, TracingLoggerMiddleware],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(RequestIdMiddleware, TracingLoggerMiddleware)
      .forRoutes('*');
  }
}

Консольний вивід:

npm run start:dev
[Nest] 12345 - LOG [HTTP] [a1b2c3d4-e5f6-7890-abcd-ef1234567890] GET /api/users 200 - 45ms
[Nest] 12345 - LOG [HTTP] [b2c3d4e5-f6g7-8901-bcde-fg2345678901] POST /api/orders 201 - 89ms
Використання X-Request-ID є стандартною практикою в мікросервісній архітектурі. Якщо запит проходить через API Gateway → Service A → Service B → Service C, всі сервіси логують один і той же requestId, що дозволяє відстежити повний ланцюг обробки запиту через централізовані логи.

JwtAuthMiddleware: автентифікація через токени

Middleware для перевірки JWT-токенів є альтернативою Guards для базової автентифікації. Основна відмінність: middleware не блокує запит при відсутності токена, а лише прикріплює користувача до request, якщо токен валідний. Авторизацію (перевірку прав доступу) виконують Guards.

Імплементація JWT-перевірки

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class JwtAuthMiddleware implements NestMiddleware {
  constructor(private readonly jwtService: JwtService) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const authHeader = req.headers.authorization;

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      // Немає токена — продовжуємо без автентифікації
      return next();
    }

    const token = authHeader.substring(7); // Видаляємо "Bearer "

    try {
      // Перевірка та декодування токена
      const payload = await this.jwtService.verifyAsync(token, {
        secret: process.env.JWT_SECRET,
      });

      // Прикріплення користувача до request
      req.user = {
        id: payload.sub,
        email: payload.email,
        roles: payload.roles || [],
      };

      next();
    } catch (error) {
      // Невалідний токен — продовжуємо без автентифікації
      // Guards можуть заблокувати доступ до захищених маршрутів
      next();
    }
  }
}

Встановлення залежностей:

npm install @nestjs/jwt

Реєстрація у модулі:

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { JwtAuthMiddleware } from './middleware/jwt-auth.middleware';

@Module({
  imports: [
    JwtModule.register({
      secret: process.env.JWT_SECRET,
      signOptions: { expiresIn: '1h' },
    }),
  ],
  providers: [JwtAuthMiddleware],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(JwtAuthMiddleware)
      .forRoutes('*'); // Застосувати до всіх маршрутів
  }
}

Використання у контролері

Після обробки middleware користувач доступний у request:

import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';

@Controller('profile')
export class ProfileController {
  @Get()
  getProfile(@Req() req: Request) {
    if (!req.user) {
      return { message: 'Not authenticated' };
    }

    return {
      id: req.user.id,
      email: req.user.email,
      roles: req.user.roles,
    };
  }
}
JwtAuthMiddleware не блокує доступ до маршрутів при відсутності токена — це відповідальність Guards. Middleware лише виконує декодування токена та прикріплення користувача. Для захисту маршрутів використовуйте @UseGuards(JwtAuthGuard) або перевіряйте req.user у контролері.

Розширений middleware з запитом до бази даних

Для отримання актуальної інформації про користувача (статус, права, блокування):

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';

@Injectable()
export class ExtendedJwtAuthMiddleware implements NestMiddleware {
  constructor(
    private readonly jwtService: JwtService,
    private readonly usersService: UsersService,
  ) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const authHeader = req.headers.authorization;

    if (!authHeader?.startsWith('Bearer ')) {
      return next();
    }

    const token = authHeader.substring(7);

    try {
      const payload = await this.jwtService.verifyAsync(token);

      // Запит до БД для перевірки статусу користувача
      const user = await this.usersService.findById(payload.sub);

      if (!user || user.isBlocked) {
        // Користувач видалений або заблокований
        return next();
      }

      req.user = {
        id: user.id,
        email: user.email,
        roles: user.roles,
        isActive: user.isActive,
      };

      next();
    } catch {
      next();
    }
  }
}
Запит до БД на кожному запиті може значно сповільнити API. Для оптимізації використовуйте:
  • Redis-кеш для зберігання даних користувача з TTL 5-15 хвилин
  • Refresh Token механізм для оновлення токенів без запитів до БД
  • Denormalized JWT Payload — зберігайте критичну інформацію (roles, permissions) безпосередньо в токені

CorsMiddleware: обробка міжсайтових запитів

Cross-Origin Resource Sharing (CORS) — це механізм безпеки браузерів, що блокує запити з інших доменів. Для дозволу запитів з фронтенд-застосунків потрібно налаштувати CORS headers.

Базовий CORS через вбудовану функцію NestJS

Найпростіший спосіб — використати вбудовану підтримку CORS:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Увімкнення CORS для всіх доменів (development)
  app.enableCors();

  await app.listen(3000);
}
bootstrap();

Налаштування CORS для production

Для production-середовища обмежте дозволені домени:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.enableCors({
    origin: [
      'https://myapp.com',
      'https://www.myapp.com',
      'https://admin.myapp.com',
    ],
    methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    credentials: true, // Дозволити передачу cookies
    maxAge: 3600, // Кешування preflight-запитів (1 година)
  });

  await app.listen(3000);
}

Кастомний CorsMiddleware з динамічною перевіркою

Для складнішої логіки (наприклад, перевірка домену в БД):

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class CustomCorsMiddleware implements NestMiddleware {
  private readonly allowedOrigins = [
    'https://myapp.com',
    'https://admin.myapp.com',
  ];

  use(req: Request, res: Response, next: NextFunction) {
    const origin = req.headers.origin;

    if (origin && this.allowedOrigins.includes(origin)) {
      res.setHeader('Access-Control-Allow-Origin', origin);
      res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE');
      res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
      res.setHeader('Access-Control-Allow-Credentials', 'true');
    }

    // Обробка preflight-запитів
    if (req.method === 'OPTIONS') {
      return res.status(204).end();
    }

    next();
  }
}
Preflight-запити — це додаткові OPTIONS-запити, які браузер відправляє перед основним запитом для перевірки дозволів CORS. Вони відбуваються для:
  • Методів, крім GET/POST/HEAD
  • Кастомних заголовків (наприклад, Authorization)
  • Content-Type, крім application/x-www-form-urlencoded, multipart/form-data, text/plain
Middleware має відповідати 204 No Content на OPTIONS-запити без виклику обробника.

Візуалізація CORS Preflight Flow

Loading diagram...
sequenceDiagram
    participant Browser as Браузер<br/>(https://myapp.com)
    participant Server as NestJS Server<br/>(https://api.myapp.com)
    
    Note over Browser: Користувач викликає<br/>fetch('/api/users', { method: 'DELETE' })
    
    Browser->>Server: OPTIONS /api/users<br/>Origin: https://myapp.com
    activate Server
    Server->>Server: CorsMiddleware:<br/>Перевірка origin
    alt Origin дозволений
        Server-->>Browser: 204 No Content<br/>Access-Control-Allow-Origin: https://myapp.com<br/>Access-Control-Allow-Methods: DELETE
        deactivate Server
        
        Browser->>Server: DELETE /api/users<br/>Authorization: Bearer token
        activate Server
        Server->>Server: JwtAuthMiddleware<br/>→ Guard → Handler
        Server-->>Browser: 200 OK + JSON
        deactivate Server
    else Origin заборонений
        Server-->>Browser: 403 Forbidden
        deactivate Server
        Note over Browser: Браузер блокує запит<br/>CORS error
    end

Інтеграція сторонніх Middleware

NestJS сумісний з екосистемою Express middleware, що дозволяє використовувати перевірені рішення для типових задач.

Helmet: безпека через HTTP-заголовки

Helmet встановлює безпечні HTTP-заголовки для захисту від поширених атак:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import helmet from 'helmet';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Базова конфігурація Helmet
  app.use(helmet());

  await app.listen(3000);
}
bootstrap();

Встановлення:

npm install helmet

Які заголовки встановлює Helmet:

ЗаголовокПризначення
Content-Security-PolicyЗахист від XSS через обмеження джерел скриптів
X-Frame-OptionsЗахист від clickjacking (заборона вбудовування в iframe)
X-Content-Type-OptionsЗаборона MIME-sniffing браузерами
Strict-Transport-SecurityПримусове використання HTTPS
X-DNS-Prefetch-ControlКонтроль DNS prefetching

Налаштована конфігурація для production:

app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", "'unsafe-inline'", 'https://cdn.example.com'],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", 'data:', 'https:'],
        connectSrc: ["'self'", 'https://api.example.com'],
      },
    },
    hsts: {
      maxAge: 31536000, // 1 рік
      includeSubDomains: true,
      preload: true,
    },
  })
);
Helmet з дефолтною конфігурацією може заблокувати завантаження зовнішніх скриптів (Google Analytics, CDN). Налаштуйте contentSecurityPolicy відповідно до вашого фронтенду або вимкніть його:
app.use(
  helmet({
    contentSecurityPolicy: false, // Вимкнути CSP
  })
);

Compression: стиснення відповідей

Compression middleware стискає HTTP-відповіді за допомогою gzip або brotli, зменшуючи обсяг переданих даних на 60-80%:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as compression from 'compression';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Увімкнення gzip-стиснення для всіх відповідей
  app.use(compression());

  await app.listen(3000);
}
bootstrap();

Встановлення:

npm install compression
npm install -D @types/compression

Налаштована конфігурація:

app.use(
  compression({
    level: 6, // Рівень стиснення (0-9, де 9 — максимальне)
    threshold: 1024, // Стискати відповіді більше 1KB
    filter: (req, res) => {
      // Стискати лише JSON та HTML
      const contentType = res.getHeader('Content-Type') as string;
      if (!contentType) return false;
      return /json|text|html/.test(contentType);
    },
  })
);

Порівняння розміру відповіді:

Тип контентуБез стисненняЗ gzipЗменшення
JSON (100 записів)45 KB8 KB82%
HTML сторінка120 KB25 KB79%
JavaScript бандл850 KB280 KB67%
Compression middleware підтримує Brotli (більш ефективний алгоритм стиснення за gzip), якщо клієнт вказує Accept-Encoding: br. Brotli дає додаткові 15-20% зменшення розміру порівняно з gzip, але потребує більше CPU для стиснення.

Morgan: HTTP-логер у стилі Apache/Nginx

Morgan — це найпопулярніший HTTP-логер для Express з підтримкою стандартних форматів логів:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as morgan from 'morgan';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Combined формат (Apache Combined Log Format)
  app.use(morgan('combined'));

  await app.listen(3000);
}
bootstrap();

Встановлення:

npm install morgan
npm install -D @types/morgan

Доступні формати:

app.use(morgan('combined'));
// ::1 - - [05/Sep/2026:10:30:00 +0000] "GET /api/users HTTP/1.1" 200 1234 "-" "Mozilla/5.0"

Кастомний формат логів:

app.use(
  morgan(':method :url :status :res[content-length] - :response-time ms')
);
// GET /api/users 200 1234 - 45.123 ms

Запис логів у файл:

import * as fs from 'fs';
import * as path from 'path';

const accessLogStream = fs.createWriteStream(
  path.join(__dirname, '..', 'logs', 'access.log'),
  { flags: 'a' } // Append mode
);

app.use(morgan('combined', { stream: accessLogStream }));
Morgan записує логи синхронно у заданий stream. Для високонавантажених API розгляньте використання асинхронних логерів (Winston, Pino) або направлення логів у зовнішні сервіси (Datadog, Loggly) через HTTP API замість запису у файл.

Rate Limiting: захист від зловживань

Rate limiting обмежує кількість запитів від одного IP-адреси для захисту від DDoS та зловживання API:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import rateLimit from 'express-rate-limit';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 хвилин
    max: 100, // Максимум 100 запитів з одного IP за 15 хвилин
    message: 'Too many requests from this IP, please try again later',
    standardHeaders: true, // Додати заголовки RateLimit-*
    legacyHeaders: false, // Вимкнути X-RateLimit-*
  });

  app.use(limiter);

  await app.listen(3000);
}
bootstrap();

Встановлення:

npm install express-rate-limit

Відповідь при перевищенні ліміту:

HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 1725537600
Retry-After: 450
Content-Type: application/json

{
  "message": "Too many requests from this IP, please try again later"
}

Різні ліміти для різних ендпоінтів:

// Суворий ліміт для автентифікації (захист від brute-force)
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5, // 5 спроб за 15 хвилин
  skipSuccessfulRequests: true, // Не рахувати успішні запити
});

// М'який ліміт для публічного API
const apiLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 100, // 100 запитів на хвилину
});

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Застосувати authLimiter лише до /auth/*
  app.use('/auth', authLimiter);
  app.use('/api', apiLimiter);

  await app.listen(3000);
}
Обмеження express-rate-limit:
  • Зберігає лічильники в пам'яті процесу — не працює в кластері Node.js без зовнішнього сховища
  • При рестарті сервера всі лічильники скидаються
Для production використовуйте Redis-backed rate limiting через пакет rate-limit-redis:
import RedisStore from 'rate-limit-redis';
import { createClient } from 'redis';

const redisClient = createClient({ url: 'redis://localhost:6379' });
await redisClient.connect();

const limiter = rateLimit({
  store: new RedisStore({
    client: redisClient,
    prefix: 'rl:',
  }),
  windowMs: 15 * 60 * 1000,
  max: 100,
});
Це забезпечує спільні лічильники між всіма екземплярами застосунку.

Body Parser Middleware: парсинг тіла запиту

NestJS автоматично використовує express.json() та express.urlencoded() для парсингу JSON та form-urlencoded тіл запитів. Проте можна налаштувати ліміти та поведінку.

Налаштування лімітів JSON

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    bodyParser: true, // За замовчуванням увімкнено
  });

  // Налаштування JSON-парсера
  app.use(express.json({ limit: '10mb' })); // Дозволити JSON до 10MB

  await app.listen(3000);
}
bootstrap();

Обробка помилок парсингу:

import * as express from 'express';

app.use(
  express.json({
    limit: '1mb',
    verify: (req, res, buf, encoding) => {
      // Перевірка валідності JSON перед парсингом
      try {
        JSON.parse(buf.toString());
      } catch (e) {
        throw new Error('Invalid JSON');
      }
    },
  })
);

Вимкнення автоматичного парсингу

Для роботи з сирими байтами (наприклад, для Stripe Webhooks):

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    bodyParser: false, // Вимкнути автоматичний парсинг
  });

  // Застосувати селективно
  app.use('/api', express.json()); // JSON для /api/*
  app.use(
    '/webhooks/stripe',
    express.raw({ type: 'application/json' }) // Сирі байти для webhooks
  );

  await app.listen(3000);
}
Чому Stripe Webhooks потребують raw body?Stripe підписує webhook-запити через HMAC-SHA256 від сирого тіла запиту. Якщо тіло розпарсене в JSON, перевірка підпису завжди провалюється, оскільки JSON-парсинг змінює форматування (пробіли, порядок ключів). Для коректної перевірки потрібен raw buffer.

Для роботи з cookies (сесії, токени, персоналізація) використовується cookie-parser:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as cookieParser from 'cookie-parser';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Базовий парсинг cookies
  app.use(cookieParser());

  // З підтримкою підписаних cookies
  app.use(cookieParser('secret-key-for-signing'));

  await app.listen(3000);
}
bootstrap();

Встановлення:

npm install cookie-parser
npm install -D @types/cookie-parser

Використання у контролері:

import { Controller, Get, Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';

@Controller('auth')
export class AuthController {
  @Get('set-token')
  setToken(@Res({ passthrough: true }) res: Response) {
    res.cookie('access_token', 'jwt-token-here', {
      httpOnly: true, // Недоступний для JavaScript
      secure: true, // Лише HTTPS
      sameSite: 'strict', // Захист від CSRF
      maxAge: 3600000, // 1 година
    });

    return { message: 'Cookie set' };
  }

  @Get('read-token')
  readToken(@Req() req: Request) {
    const token = req.cookies['access_token'];
    return { token };
  }

  @Get('read-signed')
  readSigned(@Req() req: Request) {
    // Підписані cookies (потребують секретного ключа)
    const signedToken = req.signedCookies['session_id'];
    return { signedToken };
  }
}
Безпечна конфігурація cookies для JWT:
res.cookie('refresh_token', token, {
  httpOnly: true,        // Захист від XSS
  secure: true,          // Лише HTTPS
  sameSite: 'strict',    // Захист від CSRF
  maxAge: 7 * 24 * 60 * 60 * 1000, // 7 днів
  path: '/auth/refresh', // Обмежити шлях
});
Ця конфігурація захищає від:
  • XSS (httpOnly заборонить доступ через document.cookie)
  • CSRF (sameSite заборонить відправлення cookies з інших сайтів)
  • Man-in-the-middle (secure вимагає HTTPS)

Session Middleware: управління сесіями

Для застосунків з серверними сесіями (альтернатива JWT) використовується express-session:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as session from 'express-session';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.use(
    session({
      secret: process.env.SESSION_SECRET,
      resave: false,
      saveUninitialized: false,
      cookie: {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        maxAge: 24 * 60 * 60 * 1000, // 24 години
      },
    })
  );

  await app.listen(3000);
}
bootstrap();

Встановлення:

npm install express-session
npm install -D @types/express-session

Використання сесій у контролері:

import { Controller, Get, Post, Req, Body } from '@nestjs/common';
import { Request } from 'express';

@Controller('auth')
export class AuthController {
  @Post('login')
  login(@Req() req: Request, @Body() credentials: LoginDto) {
    // Перевірка облікових даних (псевдокод)
    const user = this.authService.validateUser(credentials);

    if (user) {
      // Збереження даних у сесії
      req.session['userId'] = user.id;
      req.session['email'] = user.email;
      return { message: 'Logged in' };
    }

    return { message: 'Invalid credentials' };
  }

  @Get('profile')
  getProfile(@Req() req: Request) {
    const userId = req.session['userId'];

    if (!userId) {
      return { message: 'Not authenticated' };
    }

    return {
      userId,
      email: req.session['email'],
    };
  }

  @Post('logout')
  logout(@Req() req: Request) {
    req.session.destroy((err) => {
      if (err) {
        return { message: 'Error logging out' };
      }
    });

    return { message: 'Logged out' };
  }
}

Session Store з Redis

Для production обов'язково використовуйте зовнішнє сховище (Redis, MongoDB):

import * as session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const redisClient = createClient({ url: 'redis://localhost:6379' });
  await redisClient.connect();

  app.use(
    session({
      store: new RedisStore({ client: redisClient }),
      secret: process.env.SESSION_SECRET,
      resave: false,
      saveUninitialized: false,
      cookie: {
        httpOnly: true,
        secure: true,
        maxAge: 86400000, // 24 години
      },
    })
  );

  await app.listen(3000);
}

Встановлення Redis Store:

npm install connect-redis redis
Не використовуйте in-memory session store (MemoryStore) у production!Проблеми MemoryStore:
  • Витік пам'яті: сесії накопичуються і ніколи не видаляються автоматично
  • Не працює в кластері: кожен процес має свою пам'ять, сесії не розподіляються
  • Втрата даних при рестарті: всі сесії зникають при перезавантаженні
Завжди використовуйте Redis, MongoDB або PostgreSQL для зберігання сесій у production.

Комплексний приклад: повний middleware stack

Розглянемо реалістичну конфігурацію production-застосунку з усіма розглянутими middleware:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import helmet from 'helmet';
import * as compression from 'compression';
import * as morgan from 'morgan';
import * as cookieParser from 'cookie-parser';
import rateLimit from 'express-rate-limit';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: ['error', 'warn', 'log'], // Логи NestJS
  });

  // 1. Безпека: Helmet (перший для встановлення заголовків)
  app.use(
    helmet({
      contentSecurityPolicy: {
        directives: {
          defaultSrc: ["'self'"],
          scriptSrc: ["'self'", 'https://cdn.example.com'],
        },
      },
    })
  );

  // 2. CORS для дозволених доменів
  app.enableCors({
    origin: [
      process.env.FRONTEND_URL,
      process.env.ADMIN_URL,
    ].filter(Boolean),
    credentials: true,
  });

  // 3. Rate Limiting
  const limiter = rateLimit({
    windowMs: 15 * 60 * 1000,
    max: 100,
  });
  app.use(limiter);

  // 4. Логування запитів (Morgan у файл для production)
  if (process.env.NODE_ENV === 'production') {
    const accessLogStream = fs.createWriteStream('./logs/access.log', {
      flags: 'a',
    });
    app.use(morgan('combined', { stream: accessLogStream }));
  } else {
    app.use(morgan('dev')); // Кольоровий вивід для development
  }

  // 5. Cookie Parser для автентифікації
  app.use(cookieParser(process.env.COOKIE_SECRET));

  // 6. JSON Body Parser з лімітом
  app.use(express.json({ limit: '1mb' }));

  // 7. Compression для стиснення відповідей
  app.use(compression());

  // 8. Глобальний префікс API
  app.setGlobalPrefix('api/v1');

  await app.listen(process.env.PORT || 3000);
}
bootstrap();

Порядок виконання middleware (критично важливо!):

Крок 1: Helmet

Встановлює безпечні заголовки перед будь-якою обробкою.

Крок 2: CORS

Перевіряє Origin та відповідає на preflight-запити.

Крок 3: Rate Limiting

Блокує зловживання до парсингу тіла запиту (економія CPU).

Крок 4: Логування

Записує інформацію про запит (метод, URL, IP).

Розпарсовує cookies для автентифікації.

Крок 6: Body Parser

Перетворює JSON-тіло в об'єкт JavaScript.

Крок 7: Compression

Стискає відповідь перед відправленням клієнту.

Чому порядок важливий?
  • Rate Limiting перед Body Parser: блокування зловживання до парсингу великих JSON-тіл (економія CPU та пам'яті)
  • Helmet першим: гарантує встановлення безпечних заголовків навіть для помилкових відповідей
  • CORS після Helmet: Helmet може перезаписати деякі CORS-заголовки, тому CORS йде другим
  • Compression останнім: стискає фінальну відповідь, включаючи помилки та логи

Моніторинг продуктивності через Middleware

Для виявлення повільних ендпоінтів створіємо middleware з метриками:

import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

interface PerformanceMetrics {
  [endpoint: string]: {
    count: number;
    totalDuration: number;
    avgDuration: number;
    maxDuration: number;
  };
}

@Injectable()
export class PerformanceMiddleware implements NestMiddleware {
  private readonly logger = new Logger('Performance');
  private metrics: PerformanceMetrics = {};

  use(req: Request, res: Response, next: NextFunction) {
    const startTime = Date.now();
    const endpoint = `${req.method} ${req.path}`;

    res.on('finish', () => {
      const duration = Date.now() - startTime;

      // Оновлення метрик
      if (!this.metrics[endpoint]) {
        this.metrics[endpoint] = {
          count: 0,
          totalDuration: 0,
          avgDuration: 0,
          maxDuration: 0,
        };
      }

      const metric = this.metrics[endpoint];
      metric.count++;
      metric.totalDuration += duration;
      metric.avgDuration = metric.totalDuration / metric.count;
      metric.maxDuration = Math.max(metric.maxDuration, duration);

      // Попередження про повільні запити
      if (duration > 1000) {
        this.logger.warn(
          `Slow request detected: ${endpoint} took ${duration}ms`
        );
      }
    });

    next();
  }

  // Метод для виведення статистики (викликати через ендпоінт або cron)
  getMetrics() {
    return this.metrics;
  }
}

Використання у контролері для моніторингу:

@Controller('metrics')
export class MetricsController {
  constructor(private performanceMiddleware: PerformanceMiddleware) {}

  @Get('performance')
  getPerformanceMetrics() {
    return this.performanceMiddleware.getMetrics();
  }
}

Приклад виводу метрик:

{
  "GET /api/users": {
    "count": 150,
    "totalDuration": 6750,
    "avgDuration": 45,
    "maxDuration": 203
  },
  "POST /api/orders": {
    "count": 42,
    "totalDuration": 3780,
    "avgDuration": 90,
    "maxDuration": 450
  }
}
Для професійного моніторингу інтегруйте метрики з:
  • Prometheus через prom-client (експорт метрик у форматі Prometheus)
  • Datadog APM через dd-trace (автоматичний трейсинг)
  • New Relic через newrelic SDK
  • Elastic APM через elastic-apm-node
Ці інструменти автоматично збирають метрики, будують графіки та налаштовують алерти.

Порівняння Middleware з іншими компонентами Pipeline

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log(`${req.method} ${req.url}`);
    next();
  }
}
// Використання: застосувати до всіх маршрутів через configure()

Таблиця порівняння:

ХарактеристикаMiddlewareGuardInterceptor
Доступ до метаданих❌ Ні✅ Так✅ Так
Dependency Injection✅ Так (класовий)✅ Так✅ Так
Може блокувати запит✅ Так✅ Так✅ Так
Трансформація відповіді❌ Складно❌ Ні✅ Так
Виконання після обробника✅ Так (через події)❌ Ні✅ Так
Порядок у pipeline1-й (до маршрутизації)2-й (після маршрутизації)3-й (обгортає обробник)

Підсумок: коли використовувати кожен Middleware

LoggerMiddleware

Для логування всіх HTTP-запитів, включаючи час виконання, статус-коди та метадані.

JwtAuthMiddleware

Для декодування JWT-токенів та прикріплення користувача до request. Авторизацію виконуйте через Guards.

RequestIdMiddleware

Для трейсингу запитів у мікросервісній архітектурі через унікальні ідентифікатори.

Helmet, Compression, CORS

Для базової безпеки, оптимізації та міжсайтових запитів — використовуйте перевірені сторонні пакети.

Rate Limiting

Для захисту API від DDoS та brute-force атак через обмеження частоти запитів.

PerformanceMiddleware

Для збору метрик продуктивності та виявлення вузьких місць у обробці запитів.

У наступній лекції ми розглянемо Guards — компоненти авторизації, що приймають рішення про доступ до маршрутів на основі метаданих та прав користувача.

Copyright © 2026