Часть логики нужна всем маршрутам, но не принадлежит ни одному: завернуть ответ в единый конверт, замерить время, залогировать вызов. В NestJS это место — interceptor: он оборачивает обработку запроса, видит и вход, и выход, и может вмешаться в оба. Это идиоматичный аналог сквозной логики (AOP).

Что такое interceptor

Interceptor реализует NestInterceptor с методом intercept, который получает контекст и next — поток обработки. Вернув next.handle(), ты пропускаешь запрос дальше; обернув его RxJS-операторами, добавляешь поведение до и после.

import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    const start = Date.now();
    const { method, url } = context.switchToHttp().getRequest();
    return next.handle().pipe(
      tap(() => console.log(`${method} ${url} — ${Date.now() - start}ms`)),
    );
  }
}

Код до next.handle() выполняется на входе, операторы в pipe — на выходе. tap делает побочный эффект, не меняя ответ.

Трансформация ответа

Частая задача — единый формат ответа по всему API. Interceptor с map оборачивает результат любого обработчика.

import { map } from 'rxjs/operators';

@Injectable()
export class WrapResponseInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    return next.handle().pipe(map((data) => ({ data })));
  }
}

Теперь контроллеры возвращают «голый» результат, а конверт { data: ... } навешивается в одном месте — не дублируется в каждом методе.

Порядок в конвейере

Interceptor — один из энхансеров NestJS, и важно, где он в цепочке. Запрос проходит: middleware → guards → interceptors (часть до next.handle()) → pipes → обработчик → interceptors (часть после) → и, если брошено исключение, exception filters. Из этого следует разделение ролей: guard решает «пускать ли» (до всего), pipe проверяет данные у обработчика, interceptor оборачивает выполнение, filter ловит ошибку. Каждый — про своё, и interceptor не место для авторизации или валидации.

Подключение

Interceptor применяют точечно (@UseInterceptors на контроллере/методе) или глобально (app.useGlobalInterceptors(...) либо провайдером APP_INTERCEPTOR).

import { Controller, UseInterceptors } from '@nestjs/common';

@Controller('products')
@UseInterceptors(LoggingInterceptor)
export class ProductController { /* ... */ }

Логирование и единый конверт обычно вешают глобально; специфичные interceptor-ы — точечно.

Где это в UCP

Interceptor — это сквозная логика, вынесенная из обработчиков: логи, тайминги, формат ответа собраны в одном месте, а не размазаны по контроллерам и Handler-ам. Бизнес-логике тут не место — interceptor оборачивает выполнение, но не выражает сценарий. Это тот же приём, что аспекты (AOP) в Spring-биндинге: пересекающую логику отделяют от основной. Вместе с exception filters interceptor-ы дают чистый край сервиса, а корреляция и тайминги, проставленные здесь, питают наблюдаемость, без которой продукт-инженер не разберёт инцидент.