Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке interceptors у NestJS?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**NestJS interceptor** (перехоплювач) - клас, що обгортає виконання обробника маршруту через RxJS Observable і дозволяє додавати логіку до і після нього. ```typescript @Injectable() export class LoggingInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const now = Date.now(); return next.handle().pipe(tap(() => console.log(`Elapsed: ${Date.now() - now}ms`))); } } ``` **Головне:** все до `next.handle()` виконується до хендлера; оператори в `.pipe()` - після.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**NestJS interceptor** (перехоплювач) - клас, що обгортає виконання обробника маршруту через RxJS Observable і дозволяє запускати логіку як до хендлера, так і після нього. ## Теорія ### Коротко - Interceptors стоять у pipeline після guards і охоплюють хендлер з обох боків - `next.handle()` повертає `Observable`; все всередині `.pipe()` запускається після хендлера - Аналогія: митний контроль в аеропорту, що перевіряє багаж до посадки і після приземлення - Використовуй для логування, стандартизації відповідей, кешування та маппінгу помилок - Не замінює guards (авторизація) і pipes (валідація вхідних даних) ### Швидкий приклад ```typescript import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; @Injectable() export class LoggingInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { console.log('До хендлера'); const now = Date.now(); return next.handle().pipe( tap(() => console.log(`Після хендлера. Час: ${Date.now() - now}ms`)), ); } } ``` Код до `next.handle()` виконується першим. Колбек у `tap` запускається після того, як хендлер завершився. Ця межа і є вся модель. ### Головна відмінність від middleware Express middleware працює з сирими об'єктами `req` і `res` у лінійному ланцюгу до хендлера. Interceptors обгортають `Observable` самого хендлера, тому ти отримуєш контроль над асинхронним потоком: `retry`, `timeout`, `catchError` - все це компонується через стандартні RxJS оператори. Логіка після хендлера тут чистіша, бо ти реагуєш на те, що хендлер насправді повернув. ### Коли використовувати - Стандартна обгортка відповіді → interceptor (додати `{ data, success, timestamp }` до кожного endpoint) - Логування часу виконання по всіх маршрутах → interceptor - Коротке замикання при попаданні в кеш → interceptor (`of(cached)` пропускає хендлер) - Маппінг типів помилок (помилка БД → HTTP 503) → interceptor із `catchError` - Перевірка авторизації → guard (запускається до interceptors, може відхилити запит раніше) - Валідація вхідних даних → pipe ### Як це працює всередині Коли приходить запит, NestJS через reflection metadata збирає ланцюг interceptors і викликає кожен `intercept()` у порядку реєстрації. `CallHandler.handle()` при підписці запускає сам хендлер маршруту. Твій `.pipe()` стоїть поверх цього Observable. Помилки передаються через `throwError()`, тому `catchError` їх перехоплює, а `tap` - ні. Глобальні interceptors, зареєстровані через `APP_INTERCEPTOR`, запускаються в тому порядку, в якому вони описані в `providers`. Важливо, якщо у тебе є transform-interceptor і logging-interceptor і порядок впливає на те, що потрапляє в логи. ### Як підключити interceptor ```typescript // На рівні методу @Get() @UseInterceptors(LoggingInterceptor) findAll() { ... } // На рівні контролера @Controller('users') @UseInterceptors(TransformInterceptor) export class UsersController {} // Глобально через модуль (підтримує DI) @Module({ providers: [ { provide: APP_INTERCEPTOR, useClass: TransformInterceptor }, { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }, ], }) export class AppModule {} ``` ### Місце в pipeline запиту ``` Запит → Middleware → Guards → Interceptors (до хендлера) → Pipes → Route Handler → Interceptors (після хендлера) → Exception Filters Відповідь ``` ### Поширені помилки **Помилка 1: Виклик `.then()` на `next.handle()`** ```typescript // Неправильно return next.handle().then(data => ({ wrapped: data })); // TypeError під час виконання ``` `next.handle()` повертає `Observable`, а не `Promise`. Використовуй `.pipe(map(...))`. Це найпоширеніша помилка в NestJS interceptors. **Помилка 2: `tap()` для обробки помилок** ```typescript // Неправильно - помилки хендлера не доходять до tap() return next.handle().pipe( tap(() => { throw new Error('boom'); }), ); ``` `tap` спрацьовує тільки на успішних emission-ах. Помилки хендлера його обходять. Для будь-якої логіки на шляху помилки використовуй `catchError`, інакше виробничі помилки не будуть залоговані. **Помилка 3: Випадкове ігнорування хендлера** ```typescript // Забули повернути next.handle() при промаху кешу async intercept(ctx: ExecutionContext, next: CallHandler) { const cached = await this.cache.get(key); if (cached) return of(cached); // забули: return next.handle().pipe(...) } ``` Результат - `undefined` для всіх запитів без кешу. Обов'язково перевір, що обидві гілки щось повертають. **Помилка 4: Неправильний тип повернення для async interceptor** ```typescript // Неправильна анотація async intercept(ctx, next): Observable<any> { ... } // TypeScript виведе помилку // Правильно async intercept(ctx, next): Promise<Observable<any>> { ... } ``` Як тільки додаєш `async` до `intercept()`, тип повернення стає `Promise<Observable<any>>`, а не `Observable<any>`. TypeScript це зловить, але новачки, які вперше поєднують async/await з Observable, регулярно на це натрапляють. ### Де зустрічається в реальних проєктах - NestJS boilerplates → `TransformInterceptor` для консистентного конверту `{ success: true, data }` на кожному endpoint - Prisma + NestJS → interceptor для вимірювання часу запитів і виявлення повільних місць - BullMQ job processors → глобальний interceptor для нормалізації формату результатів задач - Error monitoring (Sentry, Datadog) → глобальний `catchError` interceptor до того, як помилки дійдуть до exception filters ### Питання на співбесіді **Q:** Де в pipeline стоять interceptors відносно guards і pipes? **A:** Після guards на шляху до хендлера і після хендлера на шляху назад. Guard може відхилити запит до того, як interceptor взагалі запуститься. Pipes йдуть між interceptors і хендлером. **Q:** Навіщо тут RxJS, а не звичайний async/await? **A:** Observables дозволяють декларативно компонувати `retry`, `timeout` і обробку помилок. З колбеками все це треба дротити вручну. Оператор `timeout` сам по собі окупає використання RxJS в будь-якому продакшн API. **Q:** Як застосувати interceptor тільки до POST-запитів? **A:** Перевір `context.switchToHttp().getRequest().method` всередині `intercept()` і умовно виклич `next.handle()`. Або використай `@UseInterceptors()` лише на методі з декоратором `@Post()`. **Q:** Яке навантаження від глобального interceptor? **A:** Приблизно 1-2ms на запит для типових transform або logging операцій. Поміряй timing-interceptором, перш ніж вважати це проблемою. **Q (senior):** Реалізуй timeout interceptor для повільних запитів до БД. **A:** `return next.handle().pipe(timeout(5000), catchError(() => { throw new HttpException('Request timeout', 408); }))`. Оператор `timeout` з RxJS кидає помилку, якщо Observable не завершився за вказані мілісекунди. ## Приклади ### Базовий: логування запитів ```typescript import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; @Injectable() export class LoggingInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const req = context.switchToHttp().getRequest(); const now = Date.now(); return next.handle().pipe( tap(() => { console.log(`${req.method} ${req.url} - ${Date.now() - now}ms`); }), ); } } ``` Підключи глобально через `{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }`. Кожен маршрут буде логувати метод, шлях і час виконання після повернення хендлера - без жодних змін в контролерах. ### Середній рівень: стандартизація відповідей API ```typescript import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; interface Response<T> { data: T; success: boolean; timestamp: string; } @Injectable() export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> { intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> { return next.handle().pipe( map((data) => ({ data, success: true, timestamp: new Date().toISOString(), })), ); } } // GET /products // До: [{ id: 1, name: 'Клавіатура' }] // Після: { data: [{ id: 1, name: 'Клавіатура' }], success: true, timestamp: "2024-..." } ``` Підключи глобально - і кожен endpoint повертає консистентну структуру. Фронтенд завжди читає `response.data` без обробки різних форматів по кожному endpoint. ### Просунутий: кешування з коротким замиканням ```typescript import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable, of } from 'rxjs'; import { tap, catchError } from 'rxjs/operators'; import { HttpException, HttpStatus } from '@nestjs/common'; import { Cache } from 'cache-manager'; @Injectable() export class CacheInterceptor implements NestInterceptor { constructor(private cacheManager: Cache) {} async intercept( context: ExecutionContext, next: CallHandler, ): Promise<Observable<any>> { const key = context.switchToHttp().getRequest().url; const cached = await this.cacheManager.get(key); if (cached) return of(cached); // хендлер не запускається return next.handle().pipe( tap(async (data) => { await this.cacheManager.set(key, data, 300); // TTL 5 хвилин }), catchError(() => { throw new HttpException('Service unavailable', HttpStatus.SERVICE_UNAVAILABLE); }), ); } } ``` Коли `of(cached)` повертає результат, хендлер маршруту взагалі не викликається. При промаху хендлер виконується, зберігає результат і повертає його. Є один нюанс: паралельні запити, що приходять до заповнення кешу, всі підуть у БД одночасно. Це проблема cache stampede, і вирішується вона окремим lock-механізмом поверх цього паттерну.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.