Kurs NestJS · Moduł 2: Routing i cykl żądania

Interceptors - kurier, który wraca z odpowiedzią

5 min czytania
W tej lekcji6

Wartownik przy rogatce liczy wchodzących. Strażnik skarbca decyduje, kto wejdzie. Obaj kończą służbę w tej samej chwili - gdy przybysz przekroczy próg. Żaden z nich nie widzi, z czym stamtąd wyjdzie.

Kurier widzi. Jedzie z rozkazem do namiestnika, czeka, aż tamten odpowie, i przywozi odpowiedź z powrotem - a po drodze może do niej coś dopisać. Interceptor to pierwszy mechanizm cyklu żądania, który wykonuje kod także po zakończeniu handlera, i to jedno zdanie tłumaczy prawie wszystko, co robi.

Interfejs i sygnatura

Interceptor to klasa implementująca interfejs NestInterceptor. Nie CanActivate - ten należy do guardów z poprzedniej lekcji; nie PipeTransform - to pipes, które poznasz zaraz po tej lekcji; nie ExceptionFilter - ten zajmuje się przechwytywaniem błędów. Interfejs wymaga jednej metody:

1import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
2import { Observable } from 'rxjs';
3
4@Injectable()
5export class AuditInterceptor implements NestInterceptor {
6  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
7    return next.handle();
8  }
9}

Sygnatura składa się z trzech części zapisywanych w tej kolejności: intercept( otwiera metodę, context: ExecutionContext, to pierwszy argument, a next: CallHandler) to drugi. Powyższy interceptor jeszcze nic nie robi - przekazuje żądanie dalej i oddaje wynik nietknięty.

ExecutionContext znasz już z guardów: to ten sam obiekt, z tymi samymi metodami getHandler() i switchToHttp(). Nowy jest CallHandler - uchwyt do reszty łańcucha. Dopóki nie wywołasz na nim handle(), handler kontrolera w ogóle się nie uruchomi.

Cztery fazy

Praca interceptora dzieli się na cztery etapy, zawsze w tej kolejności:

  1. Wywołanie metody intercept().
  2. Logika przed przekazaniem żądania (before).
  3. Wykonanie route handlera (next.handle()).
  4. Przetwarzanie odpowiedzi (after).

Podział jest widoczny w kodzie gołym okiem i wystarczy zapamiętać jedną granicę: wszystko, co napiszesz przed return next.handle(), to faza before; wszystko, co trafi do .pipe(...), to faza after. Jedna metoda obejmuje obie strony przejazdu kuriera - drogę tam i drogę z powrotem.

Dlatego właśnie interceptor potrafi zmierzyć czas obsługi żądania, czego middleware ani guard nie potrafią: zapisuje znacznik w fazie before i odczytuje go w fazie after, w tej samej metodzie i na tej samej zmiennej.

tap - działanie bez zmiany wyniku

next.handle() nie zwraca gotowych danych, tylko Observable - strumień z biblioteki RxJS, do którego odpowiedź dopiero trafi. Operacje na strumieniu dopina się metodą .pipe(), a operatorem najczęściej używanym do wykonania logiki po otrzymaniu odpowiedzi jest tap:

1@Injectable()
2export class LoggingInterceptor implements NestInterceptor {
3  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
4    const start = Date.now();
5
6    return next.handle().pipe(
7      tap(() => console.log(`Czas: ${Date.now() - start}ms`)),
8    );
9  }
10}

const start = Date.now() wykonuje się w fazie before, funkcja wewnątrz tap - w fazie after, po powrocie odpowiedzi. Różnica obu odczytów to czas obsługi żądania.

Nazwa tap pochodzi od „podsłuchu": operator podgląda przepływającą wartość i przepuszcza ją bez zmiany. To dokładnie to, czego chcemy przy logowaniu - odpowiedź klienta nie może zależeć od tego, czy akurat coś zapisujemy do konsoli. Trzej sąsiedzi z RxJS robią co innego: map podmienia wartość, filter potrafi ją wstrzymać, a reduce zwija cały strumień w jedną wartość i czeka na jego koniec - w żądaniu HTTP wartość jest jedna, więc nie ma czego zwijać.

map - zmiana kształtu odpowiedzi

Gdy odpowiedź ma naprawdę wyglądać inaczej, sięgasz po map. Typowe zastosowanie to wspólna koperta dla wszystkich endpointów:

1@Injectable()
2export class TransformInterceptor implements NestInterceptor {
3  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
4    return next.handle().pipe(
5      map((data) => ({ success: true, data })),
6    );
7  }
8}

Kontroler zwraca listę danin, a klient dostaje { success: true, data: [...] } - i tak samo przy każdym innym endpoincie, bez dopisywania choćby linijki w kontrolerach. Różnica wobec poprzedniego przykładu mieści się w jednym zdaniu: tap ogląda, map podmienia. Jeśli funkcja w tap coś zwróci, zostanie to zignorowane; to, co zwróci funkcja w map, staje się odpowiedzią.

RxJS ma tych operatorów więcej i część przydaje się w interceptorach od razu: catchError przechwytuje wyjątek z handlera, timeout przerywa zbyt długą obsługę żądania. Wszystkie wpinasz w to samo .pipe().

Podpięcie

Interceptor podpina się dekoratorem @UseInterceptors - na metodzie albo na całym kontrolerze:

1@Controller('tributes')
2@UseInterceptors(LoggingInterceptor)
3export class TributesController {
4  @Get()
5  findAll() {
6    return this.tributesService.findAll();
7  }
8}

Podajesz klasę, nie jej instancję - tworzenie zostawiasz wstrzykiwaniu zależności, dzięki czemu interceptor może mieć własne zależności w konstruktorze. Kilka interceptorów po przecinku tworzy łańcuch: faza before biegnie w kolejności zapisu, a faza after - w odwrotnej, bo strumień wraca tą samą drogą.

Gdy koperta ma obejmować całą aplikację, rejestrujesz interceptor globalnie: app.useGlobalInterceptors(new TransformInterceptor()) w main.ts albo jako provider z tokenem APP_INTERCEPTOR. Druga droga jest lepsza wszędzie tam, gdzie interceptor sam czegoś potrzebuje - provider przechodzi przez wstrzykiwanie zależności, new nie.

Podsumowanie

Kurier jedzie i wraca:

  • interceptor implementuje interfejs NestInterceptor - nie CanActivate, nie PipeTransform, nie ExceptionFilter,
  • sygnatura w kolejności: intercept( → context: ExecutionContext, → next: CallHandler),
  • cztery fazy: wywołanie intercept() → logika przed przekazaniem żądania (before) → wykonanie route handlera (next.handle()) → przetwarzanie odpowiedzi (after),
  • granica jest prosta: przed return next.handle() to before, wewnątrz .pipe(...) to after,
  • next.handle() zwraca Observable; bez jego wywołania handler się nie wykona,
  • tap to operator najczęściej używany do logiki po otrzymaniu odpowiedzi - podgląda wartość i przepuszcza bez zmiany, w odróżnieniu od map (podmienia), filter (wstrzymuje) i reduce (zwija strumień),
  • pomiar czasu: const start = Date.now() w fazie before, tap(() => ...) z drugim Date.now() w fazie after,
  • map((data) => ({ success: true, data })) opakowuje odpowiedź we wspólną kopertę,
  • podpięcie przez @UseInterceptors(LoggingInterceptor), globalnie przez useGlobalInterceptors albo token APP_INTERCEPTOR.

W następnej lekcji poznasz pipes - czwarty i ostatni mechanizm cyklu, który zajmie się pojedynczymi argumentami metody, zanim ta w ogóle się uruchomi. A na razie zapamiętaj: middleware i guard pracują na wejściu, interceptor jako jedyny jedzie w obie strony.

Kod do tej lekcji: src/interceptors.ts
1// Interceptors w NestJS - Kurierzy i Dyplomaci Imperium
2import {
3  Injectable, NestInterceptor, ExecutionContext, CallHandler,
4} from '@nestjs/common';
5import { Observable } from 'rxjs';
6import { tap, map, catchError } from 'rxjs/operators';
7
8console.log("Interceptors - przechwytywanie i modyfikacja komunikacji!");
9
10// ===========================================
11// 1. LoggingInterceptor - Kronikarz
12// ===========================================
13
14@Injectable()
15export class LoggingInterceptor implements NestInterceptor {
16  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
17    const request = context.switchToHttp().getRequest();
18    const method = request.method;
19    const url = request.url;
20    const now = Date.now();
21
22    console.log('[Before] ' + method + ' ' + url);
23
24    return next.handle().pipe(
25      tap(() => {
26        const duration = Date.now() - now;
27        console.log('[After] ' + method + ' ' + url + ' - ' + duration + 'ms');
28      }),
29    );
30  }
31}
32
33// ===========================================
34// 2. TransformInterceptor - Formatowanie odpowiedzi
35// ===========================================
36
37@Injectable()
38export class TransformInterceptor implements NestInterceptor {
39  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
40    return next.handle().pipe(
41      map(data => ({
42        success: true,
43        data,
44        timestamp: new Date().toISOString(),
45        imperium: 'Roma Aeterna',
46      })),
47    );
48  }
49}
50
51// ===========================================
52// 3. CacheInterceptor - Pamięć podręczna
53// ===========================================
54
55@Injectable()
56export class CacheInterceptor implements NestInterceptor {
57  private cache = new Map<string, { data: any; expiry: number }>();
58
59  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
60    const request = context.switchToHttp().getRequest();
61    const cacheKey = request.url;
62    const cached = this.cache.get(cacheKey);
63
64    if (cached && cached.expiry > Date.now()) {
65      console.log('[Cache] Hit: ' + cacheKey);
66      return new Observable(subscriber => {
67        subscriber.next(cached.data);
68        subscriber.complete();
69      });
70    }
71
72    console.log('[Cache] Miss: ' + cacheKey);
73    return next.handle().pipe(
74      tap(data => {
75        this.cache.set(cacheKey, {
76          data,
77          expiry: Date.now() + 60000, // 60 sekund
78        });
79      }),
80    );
81  }
82}
83
84// ===========================================
85// 4. ErrorInterceptor - Obsługa błędów
86// ===========================================
87
88@Injectable()
89export class ErrorInterceptor implements NestInterceptor {
90  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
91    return next.handle().pipe(
92      catchError(err => {
93        console.error('[Error] ' + err.message);
94        throw err;
95      }),
96    );
97  }
98}
99
100console.log("\n=== PODSUMOWANIE INTERCEPTORS ===");
101console.log("NestInterceptor - interfejs interceptora");
102console.log("next.handle() - wywołanie handlera (Observable)");
103console.log("tap() - efekty uboczne (logging)");
104console.log("map() - transformacja odpowiedzi");
105console.log("catchError() - przechwycenie błędów");
106

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Jaki interfejs implementuje interceptor w NestJS?

  2. 2. Jaki operator RxJS jest najczęściej używany w interceptorze do wykonania logiki po otrzymaniu odpowiedzi?

Zadania praktyczne w grze

  • Układanie w pionie

    Uporządkuj fazy działania interceptora w NestJS:

  • Edytor kodu

    Napisz LoggingInterceptor, który loguje czas wykonania żądania używając Date.now() przed i po next.handle()

  • Układanie w poziomie

    Ułóż elementy sygnatury metody intercept w prawidłowej kolejności:

  • Edytor kodu

    Napisz TransformInterceptor, który używa map() z RxJS do opakowania odpowiedzi w { success: true, data: response }

Przydatne artykuły