Ave, experienced legionaries! Consul Caesar.js leads you into the next depths of NestJS. Today we'll learn about Interceptors - the masterful couriers of our forum who can intercept, modify, and enrich every exchange of information in the system.
Interceptors in NestJS are like the best couriers and diplomats of the empire - they work in the shadows, can modify messages before delivery and after receipt, add secret information, and even completely change the course of communication.
Interceptors are classes implementing the
NestInterceptor interface that can: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('📜 Request intercepted by 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}Detailed logging of all operations in the application:
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(`🛡️ Legionary: ${legionary?.name || 'Unknown'} (Rank: ${legionary?.rank || 'N/A'})`);
11 console.log(`📦 Data: ${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(`✅ Response in ${duration}ms`);
19 console.log(`📜 Result: ${JSON.stringify(data)}`);
20 console.log('━'.repeat(50));
21
22 return data;
23 })
24 );
25 }
26}Standardizing API responses:
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': 'Tributes successfully retrieved from the treasury!',
31 '/legion': 'Legion ready to march!',
32 '/forum/status': 'Forum status checked!',
33 default: 'Operation completed successfully!'
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}A caching system for improved performance:
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 // Check if data is in cache and current
11 const cachedData = this.cache.get(cacheKey);
12 if (cachedData && this.isCacheValid(cachedData)) {
13 console.log(`📦 Cache HIT for key: ${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 for key: ${cacheKey}`);
22
23 return next.handle().pipe(
24 map(data => {
25 // Save in cache only for 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(`💾 Saved in 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 // Different TTL for different endpoints
48 if (url.includes('/tributes')) return 5 * 60 * 1000; // 5 minutes
49 if (url.includes('/legion')) return 2 * 60 * 1000; // 2 minutes
50 if (url.includes('/forum/status')) return 30 * 1000; // 30 seconds
51 return 60 * 1000; // 1 minute default
52 }
53
54 private isCacheValid(cachedData: any): boolean {
55 return Date.now() - cachedData.timestamp < cachedData.ttl;
56 }
57}Advanced error handling with Roman context:
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(`⚠️ Error in the forum system!`, {
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 // Set appropriate 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 || 'Unknown'
38 };
39
40 // Different error types
41 if (error instanceof UnauthorizedException) {
42 return {
43 ...baseResponse,
44 error: 'Unauthorized',
45 message: 'You do not have permissions, young legionary! 🛡️',
46 hint: 'Check your legionary token or contact the centurion',
47 action: 'login_required'
48 };
49 }
50
51 if (error instanceof ForbiddenException) {
52 return {
53 ...baseResponse,
54 error: 'Forbidden',
55 message: 'No permissions for this resource. ⚔️',
56 hint: 'You need a higher rank or special permissions',
57 action: 'rank_upgrade_needed'
58 };
59 }
60
61 if (error instanceof NotFoundException) {
62 return {
63 ...baseResponse,
64 error: 'Not Found',
65 message: 'Tribute not found! Maybe it has already been taken? 🏛️',
66 hint: 'Check the resource map or choose a different route',
67 action: 'check_resource_map'
68 };
69 }
70
71 if (error instanceof BadRequestException) {
72 return {
73 ...baseResponse,
74 error: 'Bad Request',
75 message: 'Invalid data! Check your reports! 📜',
76 hint: 'Make sure all required fields are filled in',
77 action: 'validate_input'
78 };
79 }
80
81 // General server error
82 return {
83 ...baseResponse,
84 error: 'Internal Server Error',
85 message: 'System failure! Engineers are already working on the fix! 🔧',
86 hint: 'Try again in a moment or report the problem to the consul',
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}Controlling operation execution time:
1@Injectable()
2export class TimeoutInterceptor implements NestInterceptor {
3 constructor(private timeout: number = 5000) {} // 5 seconds 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 `Operation took too long! Maximum time: ${operationTimeout/1000}s ⏰`
14 ))
15 }),
16 catchError(error => {
17 if (error instanceof TimeoutError) {
18 console.log(`⏰ Timeout for operation: ${request.url}`);
19 return throwError(() => new RequestTimeoutException(
20 'Operation was interrupted due to timeout! ⌛'
21 ));
22 }
23 return throwError(() => error);
24 })
25 );
26 }
27
28 private getOperationTimeout(url: string): number {
29 // Different timeouts for different operations
30 if (url.includes('/tributes/search')) return 10000; // 10s for search
31 if (url.includes('/legion/assign')) return 15000; // 15s for assigning legionaries
32 if (url.includes('/forum/repair')) return 30000; // 30s for repairs
33 return this.timeout; // 5s default
34 }
35}Encrypting sensitive data:
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 // Decrypt incoming data
9 if (this.shouldDecryptRequest(request)) {
10 request.body = this.decryptData(request.body);
11 }
12
13 return next.handle().pipe(
14 map(data => {
15 // Encrypt response for sensitive endpoints
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 // Simple encryption example (use crypto in production)
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: 'Use the legion key to decrypt! 🔐'
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('Invalid encrypted data!');
54 }
55 }
56}Monitoring operation performance:
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 // Log slow operations
32 if (duration > 1000) {
33 console.warn(`⚠️ Slow operation: ${endpoint} - ${duration}ms`);
34 }
35
36 // Log metrics every 100 requests
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(` Metrics for ${endpoint}:`);
42 console.log(` Average time: ${avgTime.toFixed(2)}ms`);
43 console.log(` Request count: ${current.count}`);
44 console.log(` Error rate: ${errorRate.toFixed(2)}%`);
45 }
46 }
47
48 // Endpoint to check metrics
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}1// Globally
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// On a controller
17@Controller('tributes')
18@UseInterceptors(CacheInterceptor, PerformanceInterceptor)
19export class TributesController {
20 // On a specific method
21 @Get('secret')
22 @UseInterceptors(EncryptionInterceptor)
23 getSecretTribute() {
24 return { location: 'Province of Dacia', coordinates: '45°N 25°E' };
25 }
26}