Interceptors: перехоплення та трансформація відповідей
Interceptors: перехоплення та трансформація відповідей
🎯 Мета лекції
- Зрозуміти концепцію Interceptors (перехоплювачів) як компонентів для трансформації запитів та відповідей
- Опанувати інтерфейс
NestInterceptorта методintercept(context, next) - Навчитися працювати з
CallHandlerдля виклику обробника черезhandle() - Вивчити використання RxJS операторів для трансформації даних:
map(),tap(),catchError(),timeout() - Засвоїти двофазову модель виконання: логіка before (до обробника) та after (після обробника)
- Розуміти місце Interceptors у Request Pipeline між Guards та Pipes
- Практикувати створення interceptors для логування, кешування, трансформації відповідей
🔑 Ключові терміни
- Interceptor (перехоплювач): компонент, що обгортає виклик обробника та може трансформувати запит/відповідь
- NestInterceptor (інтерфейс перехоплювача): інтерфейс з методом
intercept(context, next)для обробки запиту - CallHandler (обробник виклику): об'єкт з методом
handle(), що повертаєObservableрезультату обробника - Observable (спостережуваний потік): RxJS об'єкт для асинхронної обробки даних через функціональні оператори
- RxJS Operators (оператори RxJS): функції трансформації даних (
map,tap,catchError,timeout) - Before/After Logic (логіка до/після): можливість виконувати код до та після виклику обробника
- Response Transformation (трансформація відповіді): зміна структури даних, що повертаються клієнту
Концепція Interceptors: двофазова обробка
Interceptors у NestJS є третім компонентом у Request Pipeline, виконуючись після Guards, але до та після Pipes і обробника. Ключова особливість interceptors полягає у можливості виконувати логіку у двох фазах:
- Before (до виклику обробника): логування, валідація, модифікація запиту
- After (після виклику обробника): трансформація відповіді, кешування, логування часу виконання
На відміну від Middleware (що працює на рівні HTTP-стеку) та Guards (що приймають рішення про доступ), Interceptors мають повний контроль над результатом обробника через RxJS Observable. Це дозволяє:
- Трансформувати відповідь: обгорнути дані в єдиний формат
{ data, meta } - Кешувати результати: зберегти відповідь у Redis та повернути при наступному запиті
- Логувати час виконання: виміряти тривалість обробки запиту з точністю до мілісекунд
- Обробляти помилки: перехопити виключення та трансформувати їх у структуровані відповіді
- Встановлювати timeout: автоматично скасувати запит, що виконується занадто довго
Interceptors базуються на бібліотеці RxJS (Reactive Extensions for JavaScript), що надає потужні оператори для роботи з асинхронними потоками даних. Навіть якщо обробник повертає звичайне значення або Promise, NestJS автоматично обгортає його в Observable для уніфікованої обробки.
Анатомія Interceptor: інтерфейс NestInterceptor
Кожен interceptor реалізує інтерфейс NestInterceptor<T, R>, що містить метод intercept():
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class SimpleInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before handler execution');
// Виклик обробника через next.handle()
return next.handle().pipe(
// RxJS оператори для трансформації результату
);
}
}
Ключові компоненти:
@Injectable(): дозволяє використовувати Dependency InjectionNestInterceptor<T, R>: generic-інтерфейс, де T — тип вхідних даних, R — тип результатуintercept(context, next): метод, що обгортає виклик обробникаExecutionContext: об'єкт з метаданими про запит (той самий, що у Guards)CallHandler: об'єкт з методомhandle(), що викликає обробник та повертаєObservableObservable<any>: результат має бути обгорнутий у RxJS Observable
CallHandler та метод handle()
CallHandler.handle() є центральним механізмом interceptors — цей метод викликає наступний interceptor або обробник маршруту:
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// Логіка BEFORE: виконується до виклику обробника
console.log('Interceptor: before handler');
// Виклик обробника
const now = Date.now();
return next.handle().pipe(
// Логіка AFTER: виконується після виклику обробника
tap(() => console.log(`Execution time: ${Date.now() - now}ms`))
);
}
Поведінка next.handle():
- Повертає
Observable, що емітує результат обробника - Якщо не викликати
next.handle(), обробник ніколи не виконається - Можна обгорнути
next.handle()у RxJS оператори для трансформації результату
next.handle(), запит зависне назавжди. Клієнт чекатиме до timeout, а обробник контролера не буде викликано. Це схоже на middleware без виклику next().RxJS оператори для трансформації
Interceptors використовують RxJS оператори для обробки результату обробника. Ось найпоширеніші:
map(): трансформація даних
Найпростіший оператор для зміни структури відповіді:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => ({
success: true,
timestamp: new Date().toISOString(),
data, // Оригінальні дані з обробника
}))
);
}
}
Обробник повертає:
@Get()
findAll() {
return [{ id: 1, name: 'John' }];
}
Клієнт отримує після трансформації:
{
"success": true,
"timestamp": "2026-09-05T12:30:00.123Z",
"data": [
{ "id": 1, "name": "John" }
]
}
tap(): побічні ефекти без зміни даних
Виконує логіку (логування, аналітику) без зміни результату:
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger('HTTP');
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url } = request;
const now = Date.now();
return next.handle().pipe(
tap({
next: (data) => {
// Виконується при успішному результаті
this.logger.log(`${method} ${url} - ${Date.now() - now}ms`);
},
error: (error) => {
// Виконується при помилці
this.logger.error(`${method} ${url} - Error: ${error.message}`);
},
})
);
}
}
tap() не змінює дані, лише виконує побічні ефекти (логування, метрики, аналітику).
catchError(): обробка помилок
Перехоплює виключення з обробника та трансформує їх:
import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';
@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError(err => {
// Логування помилки
console.error('Caught error:', err);
// Трансформація помилки
return throwError(() => new BadRequestException('Custom error message'));
})
);
}
}
timeout(): обмеження часу виконання
Автоматично скасовує запит, що виконується занадто довго:
import { timeout } from 'rxjs/operators';
import { TimeoutError } from 'rxjs';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000), // Максимум 5 секунд
catchError(err => {
if (err instanceof TimeoutError) {
throw new RequestTimeoutException('Request took too long');
}
throw err;
})
);
}
}
HTTP-відповідь при timeout:
HTTP/1.1 408 Request Timeout
Content-Type: application/json
{
"statusCode": 408,
"message": "Request took too long",
"error": "Request Timeout"
}
Двофазова модель: before та after
Interceptor може виконувати логіку до та після виклику обробника:
@Injectable()
export class DualPhaseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// ========== BEFORE PHASE ==========
const request = context.switchToHttp().getRequest();
console.log('BEFORE: Request received', request.method, request.url);
// Модифікація request (рідко використовується)
request['customData'] = 'Added by interceptor';
const startTime = Date.now();
// ========== HANDLER EXECUTION ==========
return next.handle().pipe(
// ========== AFTER PHASE ==========
tap(data => {
const duration = Date.now() - startTime;
console.log('AFTER: Response sent in', duration, 'ms');
console.log('AFTER: Response data:', data);
})
);
}
}
Консольний вивід:
- Логування вхідних даних
- Валідації заголовків
- Встановлення метаданих запиту
- Трансформації відповіді
- Логування часу виконання
- Кешування результатів
- Збору метрик продуктивності
Застосування Interceptors: декоратор @UseInterceptors()
Interceptors застосовуються через декоратор @UseInterceptors() на трьох рівнях: метод, контролер або глобально.
Застосування на рівні методу
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { TransformInterceptor } from './interceptors/transform.interceptor';
@Controller('users')
export class UsersController {
@Get()
@UseInterceptors(TransformInterceptor) // Застосувати лише до цього методу
findAll() {
return [{ id: 1, name: 'John' }];
}
@Get(':id')
findOne(@Param('id') id: string) {
// Без interceptor
return { id, name: 'John' };
}
}
Застосування на рівні контролера
@Controller('products')
@UseInterceptors(TransformInterceptor) // Застосувати до всіх методів
export class ProductsController {
@Get()
findAll() {
return ['Product 1', 'Product 2'];
}
@Get(':id')
findOne(@Param('id') id: string) {
return `Product ${id}`;
}
}
Глобальні Interceptors
Для застосування до всіх маршрутів застосунку:
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from './interceptors/logging.interceptor';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}
Комбінування кількох Interceptors
Можна застосувати кілька interceptors, які виконуватимуться послідовно:
@Get()
@UseInterceptors(LoggingInterceptor, TransformInterceptor, CacheInterceptor)
findAll() {
return ['Data 1', 'Data 2'];
}
Порядок виконання (стек):
LoggingInterceptor(before) →TransformInterceptor(before) →CacheInterceptor(before) →- Handler execution →
CacheInterceptor(after) →TransformInterceptor(after) →LoggingInterceptor(after)
Interceptors виконуються у стековому порядку: перший викликаний — останній завершений.
LoggingInterceptor {
TransformInterceptor {
CacheInterceptor {
Handler()
}
}
}
Місце Interceptors у Request Pipeline
Interceptors займають третє місце у конвеєрі обробки запитів:
Послідовність виконання:
- Middleware — парсинг, логування, автентифікація
- Guards — авторизація, перевірка прав доступу
- Interceptors (before) — логування, підготовка даних
- Pipes — валідація та трансформація параметрів
- Route Handler — бізнес-логіка
- Interceptors (after) — трансформація відповіді, кешування
- Exception Filters — обробка помилок
- Before: підготувати дані до валідації Pipes
- After: трансформувати результат Handler незалежно від його типу (Promise, Observable, синхронне значення)
Відмінності Interceptors від інших компонентів
export function loggerMiddleware(req, res, next) {
console.log(`${req.method} ${req.url}`);
next(); // Продовжити до наступного
}
// Немає доступу до результату обробника
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const user = context.switchToHttp().getRequest().user;
return user?.roles?.includes('admin');
}
}
// Повертає лише true/false
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
return next.handle().pipe(
map(data => ({ success: true, data }))
);
}
}
// Повний контроль над результатом через RxJS
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
// Валідація окремого параметра
return value;
}
}
// Працює на рівні параметрів, а не відповідей
Порівняльна таблиця:
| Характеристика | Middleware | Guard | Interceptor | Pipe |
|---|---|---|---|---|
| Доступ до результату Handler | ❌ Ні | ❌ Ні | ✅ Так | ❌ Ні |
| Трансформація відповіді | ❌ Складно | ❌ Ні | ✅ Так | ❌ Ні (лише параметри) |
| RxJS Observable | ❌ Ні | ❌ Ні | ✅ Так | ❌ Ні |
| Логіка before/after | ✅ Так (через події) | ❌ Ні | ✅ Так | ❌ Ні |
| ExecutionContext | ❌ Ні | ✅ Так | ✅ Так | Частково |
| Типове призначення | Парсинг, CORS, логування | Авторизація | Трансформація, кешування | Валідація |
Коли використовувати Interceptors
- Трансформація відповідей: обгортання даних у єдиний формат для всього API
- Логування часу виконання: вимірювання продуктивності обробників
- Кешування результатів: збереження відповідей у Redis для швидкого доступу
- Обробка помилок: перехоплення виключень та форматування у структуровані відповіді
- Timeout: автоматичне скасування запитів, що виконуються занадто довго
- Response mapping: додавання метаданих (pagination, timestamps) до відповідей
Коли НЕ використовувати Interceptors
- Автентифікація: краще використовувати Middleware (виконується раніше)
- Авторизація: використовуйте Guards (чіткіша семантика блокування)
- Валідація параметрів: використовуйте Pipes (призначені саме для цього)
- Статичні заголовки: використовуйте Middleware (швидше та простіше)
Практичний приклад: LoggingInterceptor
Розглянемо повноцінний interceptor для логування запитів з часом виконання:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger('HTTP');
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url, body, query, params } = request;
const userAgent = request.get('user-agent') || '';
const ip = request.ip;
const startTime = Date.now();
this.logger.log(
`[REQUEST] ${method} ${url} - IP: ${ip} - UserAgent: ${userAgent}`
);
if (Object.keys(body || {}).length > 0) {
this.logger.debug(`[REQUEST BODY] ${JSON.stringify(body)}`);
}
return next.handle().pipe(
tap({
next: (data) => {
const response = context.switchToHttp().getResponse();
const { statusCode } = response;
const duration = Date.now() - startTime;
this.logger.log(
`[RESPONSE] ${method} ${url} ${statusCode} - ${duration}ms`
);
if (data !== undefined) {
this.logger.debug(
`[RESPONSE DATA] ${JSON.stringify(data).substring(0, 200)}`
);
}
},
error: (error) => {
const duration = Date.now() - startTime;
this.logger.error(
`[ERROR] ${method} ${url} - ${error.message} - ${duration}ms`,
error.stack
);
},
})
);
}
}
Консольний вивід:
Тестування Interceptors
import { Test } from '@nestjs/testing';
import { of } from 'rxjs';
import { TransformInterceptor } from './transform.interceptor';
describe('TransformInterceptor', () => {
let interceptor: TransformInterceptor;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [TransformInterceptor],
}).compile();
interceptor = module.get<TransformInterceptor>(TransformInterceptor);
});
it('should transform response to include success and timestamp', (done) => {
const mockContext = {} as any;
const mockNext = {
handle: () => of({ id: 1, name: 'John' }),
};
interceptor
.intercept(mockContext, mockNext)
.subscribe((result) => {
expect(result).toHaveProperty('success', true);
expect(result).toHaveProperty('timestamp');
expect(result).toHaveProperty('data');
expect(result.data).toEqual({ id: 1, name: 'John' });
done();
});
});
});
Підсумок
Трансформація
Before/After
RxJS
map, tap, catchError, timeout.Observable
Interceptor не може змінити параметри, що передаються Handler (це робота Pipes). Проте можна:
- Модифікувати
requestу before phase (додати дані) - Трансформувати результат Handler у after phase через RxJS оператори
Для зміни параметрів використовуйте Pipes або Middleware.
Middleware:
- Виконується до маршрутизації
- Немає доступу до результату Handler
- Працює на рівні HTTP-стеку (Express/Fastify)
Interceptor:
- Виконується після маршрутизації
- Має повний доступ до результату через Observable
- Працює на рівні NestJS з ExecutionContext та Reflector
У наступній лекції ми розглянемо практичні приклади Interceptors для кешування, трансформації відповідей, обробки помилок та моніторингу продуктивності.