Skip to main content

Що таке 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-запити до них просто не доходять

Швидкий приклад

typescript
// 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-запити провалюються.
  • Бізнес-логіка -> ні те, ні інше. Вона належить у сервіси.

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

АспектMiddlewareGuards
Порядок виконанняПерший - рівень 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() модуля:

typescript
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():

typescript
// Неправильно - запит зависає назавжди use(req: Request) { console.log('logged'); // next() не викликається } // Правильно use(req: Request, res: Response, next: NextFunction) { console.log('logged'); next(); }

Це причина більшості «мій NestJS-сервер перестав відповідати» на Stack Overflow.

Передача екземпляра замість класу:

typescript
// Неправильно - ламає DI, NestJS не може інжектити залежності consumer.apply(new LoggerMiddleware()).forRoutes('*'); // Правильно - NestJS сам створить екземпляр і зробить ін'єкцію consumer.apply(LoggerMiddleware).forRoutes('*');

Guards для CORS:

typescript
// Неправильно - виконується надто пізно, OPTIONS preflight провалюється @UseGuards(CorsGuard) @Get() findAll() {} // Правильно consumer.apply(cors()).forRoutes('*'); // або в main.ts: app.enableCors()

Async middleware без передачі помилок:

typescript
// Ризиковано - кинуті помилки зникають у 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 для логування із таймінгом

typescript
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

typescript
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

typescript
// 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 на контролері все одно запускається для валідних запитів - так отримуємо два незалежні шари контролю доступу без змішування відповідальностей.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?