Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке middleware у NestJS і чим воно відрізняється від guards?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Middleware в NestJS** запускається на рівні Express, до guards, pipes та interceptors. Має доступ до `req`, `res` і `next()`, але не може читати метадані маршруту або `ExecutionContext`. ```typescript @Injectable() export class LoggerMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { console.log(`${req.method} ${req.url}`); // виконується до будь-якого guard next(); } } ``` **Головне:** middleware - для логування, CORS і парсингу запитів. Guards - для автентифікації і перевірки ролей.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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-запити провалюються. - Бізнес-логіка -> ні те, ні інше. Вона належить у сервіси. ### Таблиця порівняння | Аспект | 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()` модуля: ```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` на контролері все одно запускається для валідних запитів - так отримуємо два незалежні шари контролю доступу без змішування відповідальностей.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.