Що таке middleware у NestJS і чим воно відрізняється від guards?
Middleware в NestJS - це функція, яка перехоплює кожен HTTP-запит до того, як він потрапляє до обробника маршруту, з прямим доступом до req, res і next() з Express.
Теорія
TL;DR
- Middleware запускається на рівні Express, до будь-якої обробки NestJS
- Guards запускаються після middleware, для кожного маршруту окремо, з доступом до
ExecutionContextі метаданих маршруту - Аналогія: middleware - черга безпеки в аеропорту (кожен пасажир проходить), guards - агенти біля воріт (перевіряють квитки для кожного рейсу)
- Вибір: логування, CORS, парсинг -> middleware. Перевірка JWT, ролей -> guards
- Ніколи не використовуй guards для CORS - preflight-запити до них просто не доходять
Швидкий приклад
// logger.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`${req.method} ${req.url} at ${new Date().toISOString()}`);
// Виведе: "GET /users at 2024-01-15T12:00:00Z"
next(); // без цього запит зависне назавжди
}
}
// app.module.ts - виконується до будь-якого guard
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes('*');
}
}Кожен запит потрапляє в цей лог до того, як NestJS перевіряє автентифікацію. Виклик next() просуває запит далі по pipeline.
Головна різниця
Middleware (проміжний обробник) виконується на рівні чистого Express. Він отримує req, res і next() - і нічого більше. Guard виконується пізніше, з ExecutionContext від NestJS, який дозволяє читати декоратори, рефлектувати метадані і приймати рішення з урахуванням конкретного маршруту. Middleware не знає, до якого контролера прямує запит. Guard знає - і саме тому автентифікація належить у guards.
Коли що використовувати
- Логування для всього застосунку, CORS-заголовки, стиснення -> middleware. Застосовується до всіх відповідних маршрутів до будь-якої обробки NestJS.
- Перевірка JWT, доступ за ролями, перевірка прав власності -> guards. Вони можуть читати метадані декоратора
@Roles()і кидати правильнийForbiddenException. - Додавання request ID, отримання tenant ID з піддомену -> middleware. Виконується достатньо рано, щоб прикріпити дані для подальших обробників.
- CORS конкретно -> завжди middleware або
app.enableCors(). Guards спрацьовують надто пізно, preflight-запити провалюються. - Бізнес-логіка -> ні те, ні інше. Вона належить у сервіси.
Таблиця порівняння
| Аспект | Middleware | Guards |
|---|---|---|
| Порядок виконання | Перший - рівень Express, до pipes | Після middleware, до обробника |
| Область дії | Всі або конкретні шляхи/модулі | На маршрут, контролер або глобально |
| Доступ | req, res, next() | ExecutionContext (рівень NestJS) |
| Переривання запиту | res.send() або не викликати next() | Повернути false або кинути виняток |
| Підтримка DI | Тільки класовий | Класовий або функціональний |
| Читання метаданих маршруту | Ні | Так, через Reflector |
| Для чого | Логування, CORS, rate limiting | Автентифікація, ролі, права |
Порядок виконання
HTTP-запит
→ Middleware (в порядку реєстрації)
→ Guards
→ Interceptors (до обробника)
→ Pipes
→ Обробник маршруту
→ Interceptors (після обробника)
→ ВідповідьNestJS компілює middleware у стек Express через app.use() під капотом. Метод configure(consumer) будує маршрутизатори з відповідністю шляхів, які виконуються послідовно для кожного запиту. Guards прив'язуються пізніше через рефлексію метаданих контролерів і маршрутів - саме тому вони можуть читати кастомні декоратори.
Реєстрація middleware
Guards прикріплюються через декоратори. Middleware реєструється через метод configure() модуля:
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
// Для всіх маршрутів
consumer.apply(LoggerMiddleware).forRoutes('*');
// Для конкретного контролера, виключаючи публічні маршрути
consumer
.apply(AuthMiddleware)
.exclude(
{ path: 'auth/login', method: RequestMethod.POST },
{ path: 'auth/register', method: RequestMethod.POST },
)
.forRoutes(UsersController);
// Декілька middleware - виконуються в цьому порядку
consumer
.apply(CorsMiddleware, HelmetMiddleware, LoggerMiddleware)
.forRoutes('*');
}
}Guard, для порівняння, прикріплюється одним декоратором: @UseGuards(JwtAuthGuard).
Типові помилки
Забутий next():
// Неправильно - запит зависає назавжди
use(req: Request) {
console.log('logged');
// next() не викликається
}
// Правильно
use(req: Request, res: Response, next: NextFunction) {
console.log('logged');
next();
}Це причина більшості «мій NestJS-сервер перестав відповідати» на Stack Overflow.
Передача екземпляра замість класу:
// Неправильно - ламає DI, NestJS не може інжектити залежності
consumer.apply(new LoggerMiddleware()).forRoutes('*');
// Правильно - NestJS сам створить екземпляр і зробить ін'єкцію
consumer.apply(LoggerMiddleware).forRoutes('*');Guards для CORS:
// Неправильно - виконується надто пізно, OPTIONS preflight провалюється
@UseGuards(CorsGuard)
@Get()
findAll() {}
// Правильно
consumer.apply(cors()).forRoutes('*');
// або в main.ts: app.enableCors()Async middleware без передачі помилок:
// Ризиковано - кинуті помилки зникають у NestJS v9+
async use(req: Request, res: Response, next: NextFunction) {
await someDbCheck(); // якщо кидає помилку, вона зникне
next();
}
// Безпечно
async use(req: Request, res: Response, next: NextFunction) {
try {
await someDbCheck();
next();
} catch (err) {
next(err); // передаємо обробнику помилок Express
}
}Де зустрічається в реальних проектах
- API-шлюзи:
express-rate-limitяк middleware до обробки будь-якого маршруту - Багатотенантні застосунки: отримання tenant ID з піддомену в middleware, прикріплення до
reqдля контролерів і guards - Журнали аудиту: логування маскованих ID (
req.headers['x-user-id'].slice(0, 4) + '****') до того, як бізнес-логіка торкнеться запиту - Заголовки безпеки:
helmet()як middleware перед будь-яким обробником - Той самий застосунок, guards обробляють: перевірку JWT через
@UseGuards(JwtAuthGuard)на захищених контролерах
Підхід, який добре працює в production: легкий middleware для логування на всіх маршрутах, RateLimitMiddleware на api/*, потім guards для автентифікації. Кожен шар виконує рівно одну задачу.
Питання для поглиблення
Q: Коли в lifecycle запиту виконується middleware відносно interceptors?
A: Middleware запускається першим, на рівні Express. Далі: guards, interceptors (до обробника), pipes, сам обробник, потім interceptors (після обробника).
Q: Як застосувати middleware тільки для конкретних HTTP-методів?
A: Використовуй forRoutes({ path: '/users', method: RequestMethod.POST }). Можна додати кілька forRoutes до одного apply для різних комбінацій методу і шляху.
Q: В чому різниця між класовим і функціональним middleware за продуктивністю?
A: Функціональний middleware не має накладних витрат DI-контейнера і займає трохи менше пам'яті. Підходить для stateless-задач на кшталт простого логування. Класовий потрібен, коли треба інжектити сервіси.
Q: Чи може middleware читати кастомні декоратори або метадані маршруту?
A: Ні. Middleware не має ExecutionContext і доступу до Reflector. Якщо потрібно читати @Roles() або інші кастомні метадані, ця логіка належить у guard.
Q: У мікросервісі з gRPC-транспортом middleware застосовується?
A: Ні. Middleware прив'язаний до HTTP і конкретного адаптера (Express або Fastify). Для gRPC використовуй interceptors і обробляй метадані транспортного рівня через RpcException.
Приклади
Базовий: middleware для логування із таймінгом
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
res.on('finish', () => {
// Виводить після відповіді: "GET /api/users 200 45ms"
console.log(`${req.method} ${req.url} ${res.statusCode} ${Date.now() - start}ms`);
});
next();
}
}res.on('finish') отримує статус-код і тривалість після завершення відповіді. Middleware не блокує обробник - він чіпляється до lifecycle відповіді і логує після її завершення.
Середній рівень: middleware для request ID
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { v4 as uuid } from 'uuid';
@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
// Використовуємо ID клієнта або генеруємо новий
const requestId = (req.headers['x-request-id'] as string) || uuid();
req['requestId'] = requestId; // доступний для контролерів і сервісів
res.setHeader('x-request-id', requestId); // відправляємо назад клієнту
next();
}
}Будь-який контролер, сервіс або guard, що запускається після цього middleware, може прочитати req['requestId'] для трасування. Той самий ID з'являється в заголовку відповіді, тому клієнт може зіставляти свої логи з серверними.
Просунутий рівень: async версійний middleware у взаємодії з guards
// version.middleware.ts
@Injectable()
export class VersionMiddleware implements NestMiddleware {
async use(req: Request, res: Response, next: NextFunction) {
if (req.url.startsWith('/api/v2') && !req.headers['api-key']) {
// Перериває запит до того, як guards запустяться
return res.status(401).json({ error: 'API key required for v2 endpoints' });
}
try {
await validateApiKey(req.headers['api-key'] as string); // перевірка в DB/cache
next();
} catch (err) {
next(err); // передаємо обробнику помилок Express, не ковтаємо
}
}
}
// app.module.ts
configure(consumer: MiddlewareConsumer) {
consumer
.apply(VersionMiddleware)
.forRoutes({ path: 'api/v2/*', method: RequestMethod.ALL });
}Цей middleware зупиняє v2-запити без API-ключа до того, як вони доберуться до guard. JwtAuthGuard на контролері все одно запускається для валідних запитів - так отримуємо два незалежні шари контролю доступу без змішування відповідальностей.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.