Kurs NestJS · Moduł 2: Routing i cykl żądania
Interceptors - kurier, który wraca z odpowiedzią
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:
- Wywołanie metody
intercept(). - Logika przed przekazaniem żądania (before).
- Wykonanie route handlera (
next.handle()). - 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- nieCanActivate, niePipeTransform, nieExceptionFilter, - 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()zwracaObservable; bez jego wywołania handler się nie wykona,tapto operator najczęściej używany do logiki po otrzymaniu odpowiedzi - podgląda wartość i przepuszcza bez zmiany, w odróżnieniu odmap(podmienia),filter(wstrzymuje) ireduce(zwija strumień),- pomiar czasu:
const start = Date.now()w fazie before,tap(() => ...)z drugimDate.now()w fazie after, map((data) => ({ success: true, data }))opakowuje odpowiedź we wspólną kopertę,- podpięcie przez
@UseInterceptors(LoggingInterceptor), globalnie przezuseGlobalInterceptorsalbo tokenAPP_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");
106Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jaki interfejs implementuje interceptor w NestJS?
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 }