P

Pipes & Exception Filters

Custom validation pipes and exception filters for robust error handling in NestJS.

Code

typescript
1// pipes/validation.pipe.ts
2import {
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.ts
43import {
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.ts
63import { 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.ts
98import {
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 exceptionResponse
132 ? (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.ts
149import {
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 exception
168 ? (exception as { getStatus: () => number }).getStatus()
169 : HttpStatus.INTERNAL_SERVER_ERROR;
170
171 const message =
172 exception instanceof Error
173 ? exception.message
174 : "Internal server error";
175
176 this.logger.error(
177 `Unhandled exception: ${message}`,
178 exception instanceof Error ? exception.stack : undefined
179 );
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.ts
193import { NestFactory } from "@nestjs/core";
194import { AppModule } from "./app.module";
195
196async function bootstrap() {
197 const app = await NestFactory.create(AppModule);
198
199 // Global pipes
200 app.useGlobalPipes(new ValidationPipe());
201
202 // Global filters
203 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