← назад к разделу

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