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.
Interceptor to klasa implementująca interfejs
. Nie NestInterceptor
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:
otwiera metodę, intercept(
to pierwszy argument, a context: ExecutionContext,
to drugi. Powyższy interceptor jeszcze nic nie robi - przekazuje żądanie dalej i oddaje wynik nietknięty.next: CallHandler)
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.Praca interceptora dzieli się na cztery etapy, zawsze w tej kolejności:
intercept().next.handle()).Podział jest widoczny w kodzie gołym okiem i wystarczy zapamiętać jedną granicę: wszystko, co napiszesz przed
, to faza before; wszystko, co trafi do return next.handle()
, to faza after. Jedna metoda obejmuje obie strony przejazdu kuriera - drogę tam i drogę z powrotem..pipe(...)
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.
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ć.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().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.Kurier jedzie i wraca, @name:
NestInterceptor - nie CanActivate, nie PipeTransform, nie ExceptionFilter,intercept( → context: ExecutionContext, → next: CallHandler),intercept() → logika przed przekazaniem żądania (before) → wykonanie route handlera (next.handle()) → przetwarzanie odpowiedzi (after),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ń),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ę,@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.