Некоторая логика нужна всем маршрутам сразу: залогировать вызов, замерить время, обернуть ответ в единый конверт. Но писать это в каждом контроллере — значит дублировать код десятки раз. В NestJS для этого есть interceptors — перехватчики, которые оборачивают обработку запроса и видят и вход, и выход.
Почему нельзя просто написать в контроллере
Представьте: в каждом методе контроллера нужно логировать время выполнения. Если методов сорок — логику копируют сорок раз. Когда формат лога меняется, правят сорок мест.
Interceptor решает это иначе: одна обёртка работает для всех маршрутов сразу. Контроллер остаётся чистым — только бизнес-логика.
Как устроен interceptor
Interceptor — это класс с декоратором @Injectable(), который реализует интерфейс NestInterceptor. У него один метод: intercept. Он получает контекст запроса и next — продолжение цепочки обработки.
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 делает побочный эффект (логирование), не меняя сам ответ.
Почему здесь RxJS
next.handle() возвращает Observable — поток, который «испускает» ответ контроллера. RxJS-операторы в pipe позволяют реагировать на этот ответ: добавить побочный эффект (tap), преобразовать данные (map), поймать ошибку (catchError).
Это не усложнение ради усложнения: Observable даёт точку для вмешательства именно после того, как контроллер завершил работу, без необходимости вручную управлять промисами или колбэками.
Трансформация ответа
Другой частый случай — единый формат ответа по всему API. Без interceptor-а каждый метод возвращает данные по-своему. С ним достаточно написать один раз:
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 (вход) → Pipes → Контроллер
↓
Ответ ← ← Filters ← Interceptors (выход) ←
Каждый слой отвечает за своё:
- Guard — решает, пускать ли запрос вообще (авторизация).
- Pipe — проверяет и преобразует входные данные (валидация).
- Interceptor — оборачивает выполнение, видит и вход, и выход.
- Exception filter — ловит ошибки и формирует ответ об ошибке.
Interceptor — не место для авторизации или валидации. Каждый слой делает своё.
Подключение interceptor-а
Interceptor подключают точечно — на контроллер или отдельный метод:
import { Controller, UseInterceptors } from '@nestjs/common';
@Controller('products')
@UseInterceptors(LoggingInterceptor)
export class ProductController { /* ... */ }
Или глобально — для всего приложения. Через провайдер это удобнее, потому что NestJS внедрит зависимости автоматически:
// app.module.ts
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}
Логирование и единый конверт ответа обычно вешают глобально. Специфичную логику (например, кэширование для одного маршрута) — точечно через @UseInterceptors.
Коротко
- Interceptor оборачивает обработку запроса: код до
next.handle()— на входе, операторы вpipe— на выходе. next.handle()возвращаетObservable; RxJS-операторы позволяют трансформировать ответ или добавить побочный эффект.tap— для логирования без изменения данных;map— для трансформации ответа.- Interceptor не подходит для авторизации (это Guards) и валидации (это Pipes).
- Подключают через
@UseInterceptorsточечно или черезAPP_INTERCEPTORглобально. - Глобальный interceptor удобен для единого формата ответа и логирования тайминга по всем маршрутам.
Что почитать дальше
- Guards и аутентификация — как решать «пускать или нет» до контроллера.
- Exception filters — как ловить ошибки и формировать единый ответ об ошибке.
- Валидация и Pipes — проверка входных данных перед контроллером.