Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Interceptors w NestJS

Ave, doświadczeni legioniści! Konsul Caesar.js prowadzi was w kolejne głębiny NestJS. Dziś poznamy Interceptors - mistrzowskich kurierów naszego forum, którzy potrafią przechwytywać, modyfikować i wzbogacać każdą wymianę informacji w systemie.

Interceptors w NestJS to jak najlepsi kurierzy i dyplomaci imperium - działają w cieniu, mogą modyfikować wiadomości przed ich dostarczeniem i po otrzymaniu, dodawać tajne informacje, a nawet całkowicie zmienić przebieg komunikacji.

Czym są Interceptors?

Interceptors to klasy implementujące interfejs

NestInterceptor
, które mogą:

  • Przechwytywać żądania przed wykonaniem handlera
  • Modyfikować odpowiedzi po wykonaniu handlera
  • Rozszerzać funkcjonalność podstawowych funkcji
  • Implementować cross-cutting concerns (logging, caching, transformacje)
1import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
2import { Observable } from 'rxjs';
3import { map } from 'rxjs/operators';
4
5@Injectable()
6export class ImperiumResponseInterceptor implements NestInterceptor {
7 intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
8 console.log('📜 Przechwycenie żądania przez Interceptor');
9
10 return next.handle().pipe(
11 map(data => ({
12 ...data,
13 timestamp: new Date().toISOString(),
14 legion: 'Legio X Equestris',
15 consul: 'Caesar.js'
16 }))
17 );
18 }
19}

Logging Interceptor

Szczegółowe logowanie wszystkich operacji w aplikacji:

1@Injectable()
2export class LoggingInterceptor implements NestInterceptor {
3 intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
4 const startTime = Date.now();
5 const request = context.switchToHttp().getRequest();
6 const { method, url, body, headers } = request;
7 const legionary = request.legionary;
8
9 console.log(`⚔️ [${method}] ${url}`);
10 console.log(`🛡️ Legionista: ${legionary?.name || 'Nieznany'} (Ranga: ${legionary?.rank || 'N/A'})`);
11 console.log(`📦 Dane: ${JSON.stringify(body)}`);
12
13 return next.handle().pipe(
14 map(data => {
15 const endTime = Date.now();
16 const duration = endTime - startTime;
17
18 console.log(`✅ Odpowiedź w ${duration}ms`);
19 console.log(`📜 Wynik: ${JSON.stringify(data)}`);
20 console.log('━'.repeat(50));
21
22 return data;
23 })
24 );
25 }
26}

Response Transformation Interceptor

Standaryzacja odpowiedzi z API:

1export interface ImperiumApiResponse<T> {
2 success: boolean;
3 data: T;
4 message: string;
5 timestamp: string;
6 legion: string;
7 resource_map?: string;
8}
9
10@Injectable()
11export class ResponseTransformInterceptor<T> implements NestInterceptor<T, ImperiumApiResponse<T>> {
12 intercept(context: ExecutionContext, next: CallHandler): Observable<ImperiumApiResponse<T>> {
13 const request = context.switchToHttp().getRequest();
14 const route = request.route?.path || request.url;
15
16 return next.handle().pipe(
17 map(data => ({
18 success: true,
19 data,
20 message: this.getSuccessMessage(route),
21 timestamp: new Date().toISOString(),
22 legion: 'Legio X Equestris',
23 resource_map: this.generateResourceMap(route)
24 }))
25 );
26 }
27
28 private getSuccessMessage(route: string): string {
29 const messages = {
30 '/tributes': 'Tributy pomyślnie pobrane ze skarbca!',
31 '/legion': 'Legion gotowy do marszu!',
32 '/forum/status': 'Status forum sprawdzony!',
33 default: 'Operacja zakończona sukcesem!'
34 };
35
36 return messages[route] || messages.default;
37 }
38
39 private generateResourceMap(route: string): string {
40 return `🗺️ /api/map/${route.replace('/', '-')}-resources`
1 }
2}

Caching Interceptor

System cache'owania dla poprawy wydajności:

1@Injectable()
2export class CacheInterceptor implements NestInterceptor {
3 private cache = new Map<string, { data: any; timestamp: number; ttl: number }>();
4
5 intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
6 const request = context.switchToHttp().getRequest();
7 const cacheKey = this.generateCacheKey(request);
8 const cacheTtl = this.getCacheTtl(request.url);
9
10 // Sprawdzenie czy dane są w cache i aktualne
11 const cachedData = this.cache.get(cacheKey);
12 if (cachedData && this.isCacheValid(cachedData)) {
13 console.log(`📦 Cache HIT dla klucza: ${cacheKey}`);
14 return of({
15 ...cachedData.data,
16 cached: true,
17 cached_at: new Date(cachedData.timestamp).toISOString()
18 });
19 }
20
21 console.log(`🔍 Cache MISS dla klucza: ${cacheKey}`);
22
23 return next.handle().pipe(
24 map(data => {
25 // Zapisanie w cache tylko dla GET requests
26 if (request.method === 'GET') {
27 this.cache.set(cacheKey, {
28 data,
29 timestamp: Date.now(),
30 ttl: cacheTtl
31 });
32 console.log(`💾 Zapisano w cache: ${cacheKey}`);
33 }
34
35 return data;
36 })
37 );
38 }
39
40 private generateCacheKey(request: any): string {
41 const { method, url, query, legionary } = request;
42 const legionaryId = legionary?.id || 'anonymous';
43 return `${method}:${url}:${JSON.stringify(query)}:legionary-${legionaryId}`;
44 }
45
46 private getCacheTtl(url: string): number {
47 // Różne TTL dla różnych endpointów
48 if (url.includes('/tributes')) return 5 * 60 * 1000; // 5 minut
49 if (url.includes('/legion')) return 2 * 60 * 1000; // 2 minuty
50 if (url.includes('/forum/status')) return 30 * 1000; // 30 sekund
51 return 60 * 1000; // 1 minuta default
52 }
53
54 private isCacheValid(cachedData: any): boolean {
55 return Date.now() - cachedData.timestamp < cachedData.ttl;
56 }
57}

Error Handling Interceptor

Zaawansowana obsługa błędów z kontekstem rzymskim:

1@Injectable()
2export class ErrorHandlingInterceptor implements NestInterceptor {
3 intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
4 return next.handle().pipe(
5 catchError(error => {
6 const request = context.switchToHttp().getRequest();
7 const response = context.switchToHttp().getResponse();
8 const legionary = request.legionary;
9
10 console.error(`⚠️ Błąd w systemie forum!`, {
11 error: error.message,
12 stack: error.stack,
13 legionary: legionary?.name,
14 url: request.url,
15 method: request.method
16 });
17
18 const errorResponse = this.formatErrorResponse(error, legionary, request);
19
20 // Ustawienie odpowiedniego status code
21 const status = this.getErrorStatus(error);
22 response.status(status);
23
24 return throwError(() => errorResponse);
25 })
26 );
27 }
28
29 private formatErrorResponse(error: any, legionary: any, request: any) {
30 const baseResponse = {
31 success: false,
32 timestamp: new Date().toISOString(),
33 legion: 'Legio X Equestris',
34 consul: 'Caesar.js',
35 url: request.url,
36 method: request.method,
37 legionary: legionary?.name || 'Nieznany'
38 };
39
40 // Różne typy błędów
41 if (error instanceof UnauthorizedException) {
42 return {
43 ...baseResponse,
44 error: 'Unauthorized',
45 message: 'Nie masz uprawnień, młody legioniście! 🛡️',
46 hint: 'Sprawdź swój token legionisty lub zwróć się do centuriona',
47 action: 'login_required'
48 };
49 }
50
51 if (error instanceof ForbiddenException) {
52 return {
53 ...baseResponse,
54 error: 'Forbidden',
55 message: 'Brak uprawnień do tego zasobu. ⚔️',
56 hint: 'Potrzebujesz wyższej rangi lub specjalnych uprawnień',
57 action: 'rank_upgrade_needed'
58 };
59 }
60
61 if (error instanceof NotFoundException) {
62 return {
63 ...baseResponse,
64 error: 'Not Found',
65 message: 'Nie znaleziono tributu! Może został już zabrany? 🏛️',
66 hint: 'Sprawdź mapę zasobów lub wybierz inną trasę',
67 action: 'check_resource_map'
68 };
69 }
70
71 if (error instanceof BadRequestException) {
72 return {
73 ...baseResponse,
74 error: 'Bad Request',
75 message: 'Nieprawidłowe dane! Sprawdź swoje raporty! 📜',
76 hint: 'Upewnij się, że wszystkie wymagane pola są wypełnione',
77 action: 'validate_input'
78 };
79 }
80
81 // Ogólny błąd serwera
82 return {
83 ...baseResponse,
84 error: 'Internal Server Error',
85 message: 'Awaria w systemie! Inżynierowie już pracują nad naprawą! 🔧',
86 hint: 'Spróbuj ponownie za chwilę lub zgłoś problem konsulowi',
87 action: 'retry_later'
88 };
89 }
90
91 private getErrorStatus(error: any): number {
92 if (error instanceof HttpException) {
93 return error.getStatus();
94 }
95 return 500;
96 }
97}

Timeout Interceptor

Kontrola czasu wykonania operacji:

1@Injectable()
2export class TimeoutInterceptor implements NestInterceptor {
3 constructor(private timeout: number = 5000) {} // 5 sekund default
4
5 intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
6 const request = context.switchToHttp().getRequest();
7 const operationTimeout = this.getOperationTimeout(request.url);
8
9 return next.handle().pipe(
10 timeout({
11 each: operationTimeout,
12 with: () => throwError(() => new RequestTimeoutException(
13 `Operacja trwała zbyt długo! Maksymalny czas: ${operationTimeout/1000}s ⏰`
14 ))
15 }),
16 catchError(error => {
17 if (error instanceof TimeoutError) {
18 console.log(`⏰ Timeout dla operacji: ${request.url}`);
19 return throwError(() => new RequestTimeoutException(
20 'Operacja została przerwana z powodu przekroczenia czasu! ⌛'
21 ));
22 }
23 return throwError(() => error);
24 })
25 );
26 }
27
28 private getOperationTimeout(url: string): number {
29 // Różne timeouty dla różnych operacji
30 if (url.includes('/tributes/search')) return 10000; // 10s dla wyszukiwania
31 if (url.includes('/legion/assign')) return 15000; // 15s dla przypisywania legionistów
32 if (url.includes('/forum/repair')) return 30000; // 30s dla napraw
33 return this.timeout; // 5s default
34 }
35}

Data Encryption Interceptor

Szyfrowanie wrażliwych danych:

1@Injectable()
2export class EncryptionInterceptor implements NestInterceptor {
3 private readonly secretKey = process.env.ENCRYPTION_KEY || 'imperium-secret-key';
4
5 intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
6 const request = context.switchToHttp().getRequest();
7
8 // Deszyfrowanie danych przychodzących
9 if (this.shouldDecryptRequest(request)) {
10 request.body = this.decryptData(request.body);
11 }
12
13 return next.handle().pipe(
14 map(data => {
15 // Szyfrowanie odpowiedzi dla wrażliwych endpointów
16 if (this.shouldEncryptResponse(request)) {
17 return this.encryptData(data);
18 }
19 return data;
20 })
21 );
22 }
23
24 private shouldDecryptRequest(request: any): boolean {
25 return request.headers['content-encryption'] === 'legion-cipher';
26 }
27
28 private shouldEncryptResponse(request: any): boolean {
29 const sensitiveEndpoints = ['/tributes/locations', '/legion/secrets', '/maps/hidden'];
30 return sensitiveEndpoints.some(endpoint => request.url.includes(endpoint));
31 }
32
33 private encryptData(data: any) {
34 // Prosty przykład szyfrowania (w produkcji użyj crypto)
35 const jsonString = JSON.stringify(data);
36 const encrypted = Buffer.from(jsonString).toString('base64');
37
38 return {
39 encrypted: true,
40 data: encrypted,
41 hint: 'Użyj klucza legionu do odszyfrowania! 🔐'
42 };
43 }
44
45 private decryptData(encryptedData: any) {
46 try {
47 if (encryptedData.encrypted) {
48 const decrypted = Buffer.from(encryptedData.data, 'base64').toString('utf-8');
49 return JSON.parse(decrypted);
50 }
51 return encryptedData;
52 } catch (error) {
53 throw new BadRequestException('Nieprawidłowe dane zaszyfrowane!');
54 }
55 }
56}

Performance Monitoring Interceptor

Monitorowanie wydajności operacji:

1@Injectable()
2export class PerformanceInterceptor implements NestInterceptor {
3 private metrics = new Map<string, { totalTime: number; count: number; errors: number }>();
4 
5 intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
6 const startTime = Date.now();
7 const request = context.switchToHttp().getRequest();
8 const endpoint = `${request.method} ${request.url}`;
9 
10 return next.handle().pipe(
11 map(data => {
12 this.recordMetric(endpoint, Date.now() - startTime, false);
13 return data;
14 }),
15 catchError(error => {
16 this.recordMetric(endpoint, Date.now() - startTime, true);
17 return throwError(() => error);
18 })
19 );
20 }
21 
22 private recordMetric(endpoint: string, duration: number, isError: boolean) {
23 const current = this.metrics.get(endpoint) || { totalTime: 0, count: 0, errors: 0 };
24 
25 current.totalTime += duration;
26 current.count += 1;
27 if (isError) current.errors += 1;
28 
29 this.metrics.set(endpoint, current);
30 
31 // Logowanie wolnych operacji
32 if (duration > 1000) {
33 console.warn(`Wolna operacja: ${endpoint} - ${duration}ms`);
34 }
35 
36 // Logowanie metryk co 100 żądań
37 if (current.count % 100 === 0) {
38 const avgTime = current.totalTime / current.count;
39 const errorRate = (current.errors / current.count) * 100;
40 
41 console.log(` Metryki dla ${endpoint}:`);
42 console.log(` Średni czas: ${avgTime.toFixed(2)}ms`);
43 console.log(` Liczba żądań: ${current.count}`);
44 console.log(` Wskaźnik błędów: ${errorRate.toFixed(2)}%`);
45 }
46 }
47 
48 // Endpoint do sprawdzania metryk
49 getMetrics() {
50 const result = {};
51 this.metrics.forEach((metrics, endpoint) => {
52 result[endpoint] = {
53 averageTime: metrics.totalTime / metrics.count,
54 totalRequests: metrics.count,
55 errorRate: (metrics.errors / metrics.count) * 100
56 };
57 });
58 return result;
59 }
60}

Rejestracja Interceptors

1// Globalnie
2@Module({
3 providers: [
4 {
5 provide: APP_INTERCEPTOR,
6 useClass: LoggingInterceptor,
7 },
8 {
9 provide: APP_INTERCEPTOR,
10 useClass: ResponseTransformInterceptor,
11 },
12 ],
13})
14export class AppModule {}
15
16// Na kontrolerze
17@Controller('tributes')
18@UseInterceptors(CacheInterceptor, PerformanceInterceptor)
19export class TributesController {
20 // Na konkretnej metodzie
21 @Get('secret')
22 @UseInterceptors(EncryptionInterceptor)
23 getSecretTribute() {
24 return { location: 'Prowincja Dacia', coordinates: '45°N 25°E' };
25 }
26}
Przejdź do CodeWorlds