Практичні приклади Middleware
Практичні приклади 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();
}
}
Консольний вивід:
Розширений 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();
}
}
Консольний вивід з кольоровою індикацією:
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"}
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('*');
}
}
Консольний вивід:
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,
};
}
}
@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();
}
}
}
- 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();
}
}
OPTIONS-запити, які браузер відправляє перед основним запитом для перевірки дозволів CORS. Вони відбуваються для:- Методів, крім GET/POST/HEAD
- Кастомних заголовків (наприклад,
Authorization) - Content-Type, крім
application/x-www-form-urlencoded,multipart/form-data,text/plain
204 No Content на OPTIONS-запити без виклику обробника.Візуалізація CORS Preflight Flow
Інтеграція сторонніх 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,
},
})
);
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 KB | 8 KB | 82% |
| HTML сторінка | 120 KB | 25 KB | 79% |
| JavaScript бандл | 850 KB | 280 KB | 67% |
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('common'));
// ::1 - - [05/Sep/2026:10:30:00 +0000] "GET /api/users HTTP/1.1" 200 1234
app.use(morgan('dev'));
// GET /api/users 200 45ms - 1.23kb
app.use(morgan('tiny'));
// GET /api/users 200 1234 - 45ms
Кастомний формат логів:
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 }));
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);
}
- Зберігає лічильники в пам'яті процесу — не працює в кластері Node.js без зовнішнього сховища
- При рестарті сервера всі лічильники скидаються
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);
}
Cookie Parser Middleware
Для роботи з 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 };
}
}
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
- Витік пам'яті: сесії накопичуються і ніколи не видаляються автоматично
- Не працює в кластері: кожен процес має свою пам'ять, сесії не розподіляються
- Втрата даних при рестарті: всі сесії зникають при перезавантаженні
Комплексний приклад: повний 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).
Крок 5: Cookie Parser
Розпарсовує 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 через
newrelicSDK - 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()
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
const req = context.switchToHttp().getRequest();
console.log(`${req.method} ${req.url}`);
return next.handle(); // Має доступ до результату обробника
}
}
// Використання: @UseInterceptors(LoggingInterceptor)
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest();
const user = req.user;
return user?.roles?.includes('admin'); // Блокує запит
}
}
// Використання: @UseGuards(RolesGuard)
Таблиця порівняння:
| Характеристика | Middleware | Guard | Interceptor |
|---|---|---|---|
| Доступ до метаданих | ❌ Ні | ✅ Так | ✅ Так |
| Dependency Injection | ✅ Так (класовий) | ✅ Так | ✅ Так |
| Може блокувати запит | ✅ Так | ✅ Так | ✅ Так |
| Трансформація відповіді | ❌ Складно | ❌ Ні | ✅ Так |
| Виконання після обробника | ✅ Так (через події) | ❌ Ні | ✅ Так |
| Порядок у pipeline | 1-й (до маршрутизації) | 2-й (після маршрутизації) | 3-й (обгортає обробник) |
Підсумок: коли використовувати кожен Middleware
LoggerMiddleware
JwtAuthMiddleware
request. Авторизацію виконуйте через Guards.RequestIdMiddleware
Helmet, Compression, CORS
Rate Limiting
PerformanceMiddleware
Так, middleware може бути асинхронним. Позначте метод use() як async та використовуйте await для асинхронних операцій (запити до БД, зовнішні API):
async use(req: Request, res: Response, next: NextFunction) {
const user = await this.userService.findByToken(token);
req.user = user;
next();
}
Обов'язково обробляйте помилки через try-catch, інакше виключення завершить процес Node.js.
Для параметризації middleware створіть функцію-фабрику:
export function createLoggerMiddleware(prefix: string) {
return (req: Request, res: Response, next: NextFunction) => {
console.log(`[${prefix}] ${req.method} ${req.url}`);
next();
};
}
// Використання
consumer
.apply(createLoggerMiddleware('API'))
.forRoutes('*');
Це дозволяє створювати кілька екземплярів middleware з різними налаштуваннями.
NestJS за замовчуванням використовує власний логер, який може перехоплювати console.log. Для коректної роботи Morgan:
- Використовуйте формат
'dev'(має кольори для терміналу) - Переконайтеся, що Morgan зареєстрований перед ініціалізацією NestJS-логера
- Направте логи Morgan у інший stream (файл або stdout):
app.use(morgan('dev', { stream: process.stdout }));
У наступній лекції ми розглянемо Guards — компоненти авторизації, що приймають рішення про доступ до маршрутів на основі метаданих та прав користувача.