NestJS is recognized for its modular architecture and a plethora of built-in features that help developers create powerful backend applications. Among these features, interceptors stand out as a powerful tool for enhancing the functionality of your APIs. In this guide, we’ll delve deeper into what interceptors are, how they work, and some practical use cases.
What Are Interceptors?
Interceptors are a type of provider in NestJS, designed to intercept and transform incoming requests or outgoing responses. They sit between the request and the response cycle, providing a convenient way to execute custom logic, such as logging, transforming data, or managing exceptions.
Lifecycle of an Interceptor
To understand interceptors better, it's essential to grasp where they fit into the request lifecycle:
- Before request handling: Interceptors can manipulate a request before it reaches the handler (controller).
- After request handling: Interceptors can process the response once the handler processing is complete.
- Error handling: Interceptors can catch and handle errors before they propagate.
How to Create an Interceptor
Creating an interceptor in NestJS is straightforward. It involves a simple class implementing the NestInterceptor
interface. Here’s a quick example that logs the execution time of a request:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; @Injectable() export class LoggingInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const now = Date.now(); return next .handle() .pipe( tap(() => console.log(`Request executed in ${Date.now() - now}ms`)), ); } }
Breakdown of the Example
- Injectable Decorator: The
@Injectable()
decorator is necessary, allowing Nest's dependency injection system to manage the lifecycle of the interceptor. - intercept Method: This method takes an
ExecutionContext
and aCallHandler
. TheExecutionContext
provides details about the current request, such as the context of the handler being invoked. - next.handle(): This method invokes the next handler in the request pipeline, returning an
Observable
. This is crucial in order to enable the interceptor to process the response and perform side effects using RxJS operators liketap
.
Applying the Interceptor Globally
Once you’ve created an interceptor, you might want to apply it globally to affect all controllers in the application. Here’s how to do just that in your main application file:
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { LoggingInterceptor } from './logging.interceptor'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalInterceptors(new LoggingInterceptor()); await app.listen(3000); } bootstrap();
Applying Interceptors to Specific Routes
Alternatively, if you only want to use an interceptor for specific controllers or routes, you can do so by attaching it directly in the controller:
import { Controller, Get, UseInterceptors } from '@nestjs/common'; import { LoggingInterceptor } from './logging.interceptor'; @Controller('cats') @UseInterceptors(LoggingInterceptor) export class CatsController { @Get() findAll() { // Logic to return all cats return []; } }
Real-World Use Cases
Interceptors can serve various purposes in your NestJS application. Here are some common real-world use cases:
1. Transforming Responses
You might want to consistently modify the shape of your responses. For instance, wrapping all responses in a standard format:
@Injectable() export class ResponseFormatInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next.handle().pipe( map(data => ({ success: true, data, })), ); } }
2. Exception Handling
Interceptors are great for handling exceptions globally. You could log the error details or return a custom response format using the following interceptor:
@Injectable() export class ExceptionInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next.handle().pipe( catchError(err => { console.error(err); throw new HttpException('Something went wrong', HttpStatus.INTERNAL_SERVER_ERROR); }), ); } }
3. Caching Responses
One practical application of interceptors is caching responses. Here’s a simple illustration of caching:
@Injectable() export class CachingInterceptor implements NestInterceptor { private cache = new Map(); intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const key = this.generateKey(context); if (this.cache.has(key)) { return of(this.cache.get(key)); // Return cached response } return next.handle().pipe( tap(response => this.cache.set(key, response)) // Cache the response ); } private generateKey(context: ExecutionContext): string { // Logic to generate a unique key based on context return context.switchToHttp().getRequest().url; } }
Tracking and identifying cache misses usually enhances performance and optimizes resource usage.
Conclusion
Interceptors in NestJS provide a powerful foundation for adding consistent behavior across your application. Whether it’s logging, transforming data, handling exceptions, or caching responses, they help keep your application clean and maintainable. Understanding and implementing interceptors will greatly enhance your ability to structure robust and efficient backend APIs. As you build out your NestJS projects, leverage the power of interceptors to create cleaner, more modular code!