Часть логики нужна всем маршрутам, но не принадлежит ни одному: завернуть ответ в единый конверт, замерить время, залогировать вызов. В 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-ы дают чистый край сервиса, а корреляция и тайминги, проставленные здесь, питают наблюдаемость, без которой продукт-инженер не разберёт инцидент.