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

NESTJS · CROSS-CUTTING Фаза 7. Interceptors

Interceptors: сквозная логика в NestJS

TypeScript · NestJS · RxJS

Обертка запроса, RxJS pipe, время ответа, трансформация и граница AOP.

1. Interceptor оборачивает вызов handler

1 / 5

Код до next.handle() работает до метода, код после - с результатом. Это общая точка для telemetry и преобразования.

  • Контроллер не дублирует сквозную логику.
  • Interceptor получает ExecutionContext.
  • Не прячьте бизнес-правила в interceptor.
Request Interceptorbefore next.handleafter result Controller

2. RxJS pipe преобразует поток результата

2 / 5

next.handle() возвращает Observable. map меняет результат, tap добавляет side effect, catchError обрабатывает ошибку.

  • map возвращает новый результат.
  • tap не подменяет ответ.
  • Ошибку лучше классифицировать фильтром.
Observable map tap Response

3. TimingInterceptor измеряет длительность

3 / 5

До вызова сохраняется start time, после вычисляется duration. Метрика содержит route template и outcome, но не сырой URL и тело.

  • Время включает handler.
  • Теги ограничены.
  • Trace ID связывает логи.
start = now()request start next.handlecontroller workresult durationnow - start

4. TransformInterceptor задаёт форму ответа

4 / 5

Общая обёртка может содержать data и requestId. Не превращайте ошибку в успешный конверт и не меняйте доменную модель без контракта.

  • Ошибки обрабатываются фильтром.
  • Формат одинаков для всех routes.
  • Версия конверта фиксируется.
Order resultdomain DTO Transformdata: resultrequestId: 7f3a Responseconsistent

5. Сквозные правила имеют границу

5 / 5

Interceptors отвечают за telemetry, mapping и общие аспекты. Авторизация, цена и состояние заказа остаются в guards и домене.

  • Не создавайте второй service locator.
  • Не вызывайте БД без необходимости.
  • Тест проверяет поведение до и после.
Interceptortelemetry + mapping GuardauthN / authZ Servicebusiness rules Каждый аспект остаётся на своём уровне

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

Некоторая логика нужна всем маршрутам сразу: залогировать вызов, замерить время, обернуть ответ в единый конверт. Но писать это в каждом контроллере — значит дублировать код десятки раз. В NestJS для этого есть interceptors — перехватчики, которые оборачивают обработку запроса и видят и вход, и выход.

Почему нельзя просто написать в контроллере

Представьте: в каждом методе контроллера нужно логировать время выполнения. Если методов сорок — логику копируют сорок раз. Когда формат лога меняется, правят сорок мест.

Interceptor решает это иначе: одна обёртка работает для всех маршрутов сразу. Контроллер остаётся чистым — только бизнес-логика.

Как устроен interceptor

Interceptor — это класс с декоратором @Injectable(), который реализует интерфейс NestInterceptor. У него один метод: intercept. Он получает контекст запроса и next — продолжение цепочки обработки.

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

@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';

@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 контроллер ответ контроллер Interceptors Filters клиент

Перехватчик видит запрос дважды: по дороге туда и по дороге обратно. Поэтому в нём удобно мерить время и оборачивать ответ, а проверять права — раньше, в guard.

Каждый слой отвечает за своё:

  • 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 удобен для единого формата ответа и логирования тайминга по всем маршрутам.

Что почитать дальше