Interceptors: Transforming Responses
Creating interceptors to transform responses before they are sent to the client, e.g., adding metadata or formatting the data.
RxJS Observables in NestJS Interceptors
NestJS interceptors provide a powerful way to intercept and transform requests and responses in your application. They leverage RxJS Observables, enabling you to handle asynchronous data streams with ease and perform complex operations on the data flowing through your application.
What are Interceptors?
Interceptors are classes annotated with the @Injectable()
decorator and implement the NestInterceptor
interface. They can:
- Transform the response before it's sent to the client.
- Extend/modify the request lifecycle.
- Catch exceptions and handle them gracefully.
- Override the execution flow entirely.
- Cache the result of a function call.
- Log user interaction, analytics etc.
RxJS Observables and Interceptors
The core of using interceptors lies in their intercept()
method, which is required by the NestInterceptor
interface. This method receives two arguments:
context: ExecutionContext
: Provides access to the request/response cycle, allowing you to retrieve handler information (e.g., the controller method being executed).next: CallHandler
: An object with ahandle()
method. Callingnext.handle()
executes the route handler (controller method) and returns an RxJS Observable. Crucially, you must callnext.handle()
, otherwise the request will be effectively stopped.
The handle()
method of CallHandler
returns an RxJS Observable. This is where the power of RxJS comes in. You can use any of the RxJS operators (map
, tap
, catchError
, finalize
, etc.) to manipulate the data emitted by the Observable before it is sent back as a response.
Example Interceptor
Here's a simple example of an interceptor that transforms the response data to add a timestamp:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response {
data: T;
}
@Injectable()
export class TransformInterceptor implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(
map(data => ({ data, timestamp: Date.now() })),
);
}
}
Explanation:
TransformInterceptor
implementsNestInterceptor
.- The
intercept
method takes theExecutionContext
andCallHandler
as arguments. next.handle()
is called to execute the route handler and returns an Observable.- The
pipe
operator is used to chain RxJS operators. - The
map
operator transforms the data emitted by the Observable. In this case, it wraps the original data in an object that includes the data and a timestamp.
Applying the Interceptor
You can apply interceptors globally, at the controller level, or at the route handler level.
// Globally (in main.ts or app.module.ts)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { TransformInterceptor } from './transform.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new TransformInterceptor());
await app.listen(3000);
}
bootstrap();
// At the controller level (in your controller)
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { TransformInterceptor } from './transform.interceptor';
@Controller('users')
@UseInterceptors(TransformInterceptor)
export class UsersController {
@Get()
findAll() {
return [{ id: 1, name: 'John Doe' }];
}
}
// At the route handler level (in your controller)
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { TransformInterceptor } from './transform.interceptor';
@Controller('users')
export class UsersController {
@Get()
@UseInterceptors(TransformInterceptor)
findAll() {
return [{ id: 1, name: 'John Doe' }];
}
}
Manipulating Data with RxJS Operators
RxJS provides a rich set of operators that allow you to manipulate the data stream. Here are some common examples:
map
: Transforms the data emitted by the Observable. (See example above)tap
: Executes a side effect for each value emitted by the Observable, without modifying the value itself. Useful for logging or debugging.catchError
: Catches errors emitted by the Observable and allows you to handle them gracefully. You can return a new Observable to continue the stream, or re-throw the error.finalize
: Executes a function when the Observable completes or errors. Useful for cleanup tasks.delay
: Delays the emission of values from the Observable.
Example: Logging with tap
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const now = Date.now();
return next
.handle()
.pipe(
tap(() => this.logger.log(`Request completed in ${Date.now() - now}ms`)),
);
}
}
Example: Error Handling with catchError
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, BadRequestException } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorHandlingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
catchError(err => throwError(() => new BadRequestException('Something went wrong!'))),
);
}
}
Key Considerations
- Always call
next.handle()
to ensure the request processing continues. - Use RxJS operators strategically to transform, manipulate, or handle errors in the Observable stream.
- Choose the appropriate scope (global, controller, route) for applying your interceptors based on their function.
Conclusion
NestJS interceptors, combined with the power of RxJS Observables, provide a flexible and robust mechanism for managing requests and responses in your application. By leveraging RxJS operators, you can perform a wide range of operations, including data transformation, error handling, and logging, all within a clean and maintainable interceptor architecture.