Interceptors: Transforming Responses
Creating interceptors to transform responses before they are sent to the client, e.g., adding metadata or formatting the data.
Error Handling with Interceptors in NestJS
NestJS interceptors provide a powerful mechanism to intercept and transform the result of a request, including handling errors and exceptions that occur during request processing. They offer a centralized way to manage error responses, implement logging, and provide customized error messages, leading to cleaner and more maintainable code.
What are Interceptors?
Interceptors are a type of middleware in NestJS. They are classes decorated with @Injectable()
and implement the NestInterceptor
interface. Interceptors have the ability to:
- Bind extra logic before / after method execution.
- Transform the result returned from a function.
- Transform the exception being thrown.
- Extend the basic function behavior.
- Completely override a function depending on specific conditions (e.g., for caching purposes).
Error Handling with Interceptors
When dealing with errors, interceptors allow us to:
- Catch and Transform Exceptions: Interceptors can catch exceptions thrown within the request handler and transform them into a standardized error response format.
- Centralized Logging: They offer a central place to log errors and exceptions, aiding in debugging and monitoring.
- Custom Error Messages: Provide user-friendly and informative error messages instead of exposing raw exception details.
How Interceptors Handle Errors: A Deep Dive
The core of error handling within an interceptor relies on RxJS operators. Specifically, the catchError
operator is used to intercept and handle exceptions emitted by the observable returned by the request handler.
Here's a breakdown of the process:
- The request handler executes and may throw an exception.
- If an exception is thrown, NestJS catches it and passes it to the interceptor's
intercept
method. - The
intercept
method typically uses thecallHandler.handle()
method to get an observable representing the result of the request handler. - The
catchError
operator is chained to this observable. - If the observable emits an error, the
catchError
operator intercepts it. - Within the
catchError
operator, you can perform error transformation, logging, and return a new observable that emits the desired error response.
Practical Examples
1. Transforming Error Responses
This example demonstrates how to transform an exception into a standardized error response.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, HttpException, HttpStatus } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
interface ErrorResponse<T> {
statusCode: number;
message: string;
error?: T;
}
@Injectable()
export class ErrorInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
catchError(err => {
let statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'Internal Server Error';
let errorDetails: any = null;
if (err instanceof HttpException) {
statusCode = err.getStatus();
message = err.message;
errorDetails = err.getResponse();
} else {
// Log unexpected errors for debugging
console.error('Unexpected error:', err);
}
const errorResponse: ErrorResponse<any> = {
statusCode,
message,
error: errorDetails
};
return throwError(() => new HttpException(errorResponse, statusCode));
}),
);
}
}
Explanation:
- The
ErrorInterceptor
implements theNestInterceptor
interface. - The
intercept
method receives the execution context and aCallHandler
. callHandler.handle()
returns an observable that emits the result of the request handler.- The
pipe
method chains RxJS operators. catchError
catches any error emitted by the observable.- Inside
catchError
, we check if the error is an instance ofHttpException
. If so, we extract the status code and message from the exception. Otherwise, we assume it's an internal server error. - We then create a standardized error response object.
- Finally, we re-throw the error as an
HttpException
with the standardized error response. This allows NestJS to properly handle the error and send the appropriate response to the client. Note the use of `throwError(() => new HttpException(...))` to ensure proper error propagation and handling within the RxJS pipeline.
2. Logging Errors
This example shows how to log errors using an interceptor.
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();
const req = context.switchToHttp().getRequest();
const method = req.method;
const url = req.url;
return next
.handle()
.pipe(
tap(() => {
this.logger.log(`${method} ${url} ${Date.now() - now}ms`);
}, (error) => {
this.logger.error(`${method} ${url} ${Date.now() - now}ms`, error.stack); // Log the stack trace for debugging
})
);
}
}
Explanation:
- The
LoggingInterceptor
implements theNestInterceptor
interface. - The
intercept
method logs information about the request, including the method, URL, and execution time. - The
tap
operator allows us to perform side effects (logging) without modifying the emitted values. - The first argument to
tap
is executed when the observable emits a value (success). - The second argument to
tap
is executed when the observable emits an error (failure). We log the error and its stack trace for debugging.
3. Providing Custom Error Messages
This example demonstrates how to provide custom, user-friendly error messages.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, BadRequestException } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ValidationInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
catchError(err => {
if (err.name === 'ValidationError') { // Example: Check for a specific error type
const customMessage = 'Invalid input data. Please check your request.';
return throwError(() => new BadRequestException(customMessage));
}
return throwError(() => err); // Re-throw other errors
}),
);
}
}
Explanation:
- The
ValidationInterceptor
implements theNestInterceptor
interface. - The
intercept
method catches errors. - It checks if the error is a specific type (e.g.,
ValidationError
). - If it's the specific error type, it creates a custom error message and throws a
BadRequestException
. - If it's not the expected error type, it re-throws the original error, allowing other interceptors or the default error handler to process it. This is crucial to avoid inadvertently masking unexpected errors.
Registering Interceptors
Interceptors can be registered globally, at the controller level, or at the route handler level.
Globally (main.ts
):
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ErrorInterceptor, LoggingInterceptor } from './interceptors'; // Import your interceptors
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new ErrorInterceptor(), new LoggingInterceptor()); // Register globally
await app.listen(3000);
}
bootstrap();
Controller Level:
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { ErrorInterceptor } from '../interceptors/error.interceptor';
@Controller('users')
@UseInterceptors(ErrorInterceptor) // Register at controller level
export class UsersController {
@Get()
findAll(): string {
return 'This action returns all users';
}
}
Route Handler Level:
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { ErrorInterceptor } from '../interceptors/error.interceptor';
@Controller('users')
export class UsersController {
@Get()
@UseInterceptors(ErrorInterceptor) // Register for a specific route
findAll(): string {
return 'This action returns all users';
}
}
Key Considerations
- Error Handling Strategy: Define a clear and consistent error handling strategy for your application. Use interceptors to enforce this strategy.
- Error Response Format: Standardize the format of your error responses. This makes it easier for clients to handle errors consistently.
- Logging: Log errors and exceptions to aid in debugging and monitoring.
- User-Friendly Messages: Provide user-friendly error messages to improve the user experience. Avoid exposing sensitive information in error messages.
- Order of Interceptors: The order in which interceptors are applied matters. Global interceptors are applied before controller-level interceptors, which are applied before route handler-level interceptors.
- Re-throwing Errors: Be careful when catching errors. If you don't handle an error completely, make sure to re-throw it so that other interceptors or the default error handler can process it. Use `throwError(() => new HttpException(...))` for proper error propagation within RxJS.
Conclusion
Interceptors in NestJS provide a robust and flexible mechanism for handling errors and exceptions. By using interceptors, you can centralize your error handling logic, standardize error responses, implement logging, and provide custom error messages, leading to more maintainable and user-friendly applications.