P
Pipes & Exception Filters
Custom validation pipes and exception filters for robust error handling in NestJS.
Code
typescript
1// pipes/validation.pipe.ts2import {3 PipeTransform,4 Injectable,5 ArgumentMetadata,6 BadRequestException,7} from "@nestjs/common";8import { validate } from "class-validator";9import { plainToInstance } from "class-transformer";10 11@Injectable()12export class ValidationPipe implements PipeTransform<unknown> {13 async transform(value: unknown, { metatype }: ArgumentMetadata) {14 if (!metatype || !this.toValidate(metatype)) {15 return value;16 }17 18 const object = plainToInstance(metatype, value);19 const errors = await validate(object);20 21 if (errors.length > 0) {22 const messages = errors.map((error) => ({23 property: error.property,24 constraints: error.constraints,25 }));26 27 throw new BadRequestException({28 message: "Validation failed",29 errors: messages,30 });31 }32 33 return object;34 }35 36 private toValidate(metatype: Function): boolean {37 const types: Function[] = [String, Boolean, Number, Array, Object];38 return !types.includes(metatype);39 }40}41 42// pipes/parse-uuid.pipe.ts43import {44 PipeTransform,45 Injectable,46 BadRequestException,47} from "@nestjs/common";48 49@Injectable()50export class ParseUUIDPipe implements PipeTransform<string> {51 private readonly uuidRegex =52 /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;53 54 transform(value: string): string {55 if (!this.uuidRegex.test(value)) {56 throw new BadRequestException(`"${value}" is not a valid UUID`);57 }58 return value;59 }60}61 62// pipes/file-validation.pipe.ts63import { PipeTransform, Injectable, BadRequestException } from "@nestjs/common";64 65interface FileValidationOptions {66 maxSize?: number;67 allowedMimeTypes?: string[];68}69 70@Injectable()71export class FileValidationPipe implements PipeTransform {72 constructor(private options: FileValidationOptions = {}) {}73 74 transform(file: Express.Multer.File): Express.Multer.File {75 if (!file) {76 throw new BadRequestException("File is required");77 }78 79 const { maxSize = 5 * 1024 * 1024, allowedMimeTypes } = this.options;80 81 if (file.size > maxSize) {82 throw new BadRequestException(83 `File size exceeds maximum allowed size of ${maxSize / 1024 / 1024}MB`84 );85 }86 87 if (allowedMimeTypes && !allowedMimeTypes.includes(file.mimetype)) {88 throw new BadRequestException(89 `File type ${file.mimetype} is not allowed. Allowed types: ${allowedMimeTypes.join(", ")}`90 );91 }92 93 return file;94 }95}96 97// filters/http-exception.filter.ts98import {99 ExceptionFilter,100 Catch,101 ArgumentsHost,102 HttpException,103 HttpStatus,104 Logger,105} from "@nestjs/common";106import { Request, Response } from "express";107 108interface ErrorResponse {109 statusCode: number;110 message: string | string[];111 error: string;112 timestamp: string;113 path: string;114 requestId?: string;115}116 117@Catch(HttpException)118export class HttpExceptionFilter implements ExceptionFilter {119 private readonly logger = new Logger(HttpExceptionFilter.name);120 121 catch(exception: HttpException, host: ArgumentsHost) {122 const ctx = host.switchToHttp();123 const response = ctx.getResponse<Response>();124 const request = ctx.getRequest<Request>();125 const status = exception.getStatus();126 const exceptionResponse = exception.getResponse();127 128 const errorResponse: ErrorResponse = {129 statusCode: status,130 message:131 typeof exceptionResponse === "object" && "message" in exceptionResponse132 ? (exceptionResponse as Record<string, unknown>).message as string | string[]133 : exception.message,134 error: HttpStatus[status] || "Error",135 timestamp: new Date().toISOString(),136 path: request.url,137 requestId: request.headers["x-request-id"] as string,138 };139 140 this.logger.error(141 `${request.method} ${request.url} ${status} - ${JSON.stringify(errorResponse.message)}`142 );143 144 response.status(status).json(errorResponse);145 }146}147 148// filters/all-exceptions.filter.ts149import {150 ExceptionFilter,151 Catch,152 ArgumentsHost,153 HttpStatus,154 Logger,155} from "@nestjs/common";156 157@Catch()158export class AllExceptionsFilter implements ExceptionFilter {159 private readonly logger = new Logger(AllExceptionsFilter.name);160 161 catch(exception: unknown, host: ArgumentsHost) {162 const ctx = host.switchToHttp();163 const response = ctx.getResponse();164 const request = ctx.getRequest();165 166 const status =167 exception instanceof Error && "getStatus" in exception168 ? (exception as { getStatus: () => number }).getStatus()169 : HttpStatus.INTERNAL_SERVER_ERROR;170 171 const message =172 exception instanceof Error173 ? exception.message174 : "Internal server error";175 176 this.logger.error(177 `Unhandled exception: ${message}`,178 exception instanceof Error ? exception.stack : undefined179 );180 181 response.status(status).json({182 statusCode: status,183 message: process.env.NODE_ENV === "production"184 ? "Internal server error"185 : message,186 timestamp: new Date().toISOString(),187 path: request.url,188 });189 }190}191 192// Usage in main.ts193import { NestFactory } from "@nestjs/core";194import { AppModule } from "./app.module";195 196async function bootstrap() {197 const app = await NestFactory.create(AppModule);198 199 // Global pipes200 app.useGlobalPipes(new ValidationPipe());201 202 // Global filters203 app.useGlobalFilters(204 new AllExceptionsFilter(),205 new HttpExceptionFilter()206 );207 208 await app.listen(3000);209}210bootstrap();Run this example locally
$ npm install class-validator class-transformer