We use cookies to enhance your experience on the site
CodeWorlds

Pipes in NestJS

Ave, clever legionaries! Consul Caesar.js takes you on another adventure through the world of NestJS. Today we'll learn about Pipes - the masterful craftsmen of our forum who can transform raw materials into the most valuable resources and check every payload before accepting it into the system.

Pipes in NestJS are like the best craftsmen and quality controllers in the Roman Empire - they check every incoming payload, transform raw materials into useful items, and reject everything that doesn't meet our legion's standards.

What are Pipes?

Pipes are classes implementing the

PipeTransform
interface that:

  • Transform input data to the desired format
  • Validate data before passing it to the handler
  • Can throw exceptions if data is invalid
  • Execute just before the controller method runs
1import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
2
3@Injectable()
4export class ImperiumTributeValidationPipe implements PipeTransform {
5 transform(value: any, metadata: ArgumentMetadata) {
6 console.log(`📦 Checking payload: ${JSON.stringify(value)}`);
7
8 if (!this.isValidTribute(value)) {
9 throw new BadRequestException('This is not a genuine tribute of the empire! 🏛️');
10 }
11
12 return this.polishTribute(value);
13 }
14
15 private isValidTribute(tribute: any): boolean {
16 return tribute &&
17 typeof tribute.name === 'string' &&
18 typeof tribute.value === 'number' &&
19 tribute.value > 0;
20 }
21
22 private polishTribute(tribute: any) {
23 return {
24 ...tribute,
25 name: tribute.name.trim(),
26 value: Math.round(tribute.value),
27 inspected: true,
28 inspectionDate: new Date().toISOString()
29 };
30 }
31}

Built-in Pipes

NestJS provides several ready-made pipes:

ValidationPipe

Validates data using class-validator:

1import { IsString, IsNumber, IsPositive, IsOptional, Length } from 'class-validator';
2
3export class CreateTributeDto {
4 @IsString()
5 @Length(2, 50)
6 name: string;
7
8 @IsNumber()
9 @IsPositive()
10 value: number;
11
12 @IsString()
13 @IsOptional()
14 description?: string;
15
16 @IsString()
17 location: string;
18}
19
20@Controller('tributes')
21export class TributesController {
22 @Post()
23 @UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))
24 createTribute(@Body() tributeDto: CreateTributeDto) {
25 return {
26 message: 'Tribute added to the empire treasury!',
27 tribute: tributeDto
28 };
29 }
30}

ParseIntPipe

Transforms a string into a number:

1@Controller('legion')
2export class LegionController {
3 @Get(':id')
4 getLegionary(@Param('id', ParseIntPipe) id: number) {
5 return {
6 id,
7 name: `Legionary #${id}`,
8 type: typeof id // will be 'number'
9 };
10 }
11
12 @Get('rank/:level')
13 getByRank(
14 @Param('level', new ParseIntPipe({
15 errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE
16 })) level: number
17 ) {
18 if (level < 1 || level > 10) {
19 throw new BadRequestException('Rank must be between 1 and 10!');
20 }
21 return { rank: level };
22 }
23}

ParseBoolPipe

Transforms a string into a boolean:

1@Controller('forum')
2export class ForumController {
3 @Get('status')
4 getForumStatus(@Query('detailed', ParseBoolPipe) detailed: boolean) {
5 const basicStatus = {
6 name: 'Legio X Equestris',
7 consul: 'Caesar.js'
8 };
9
10 if (detailed) {
11 return {
12 ...basicStatus,
13 legionaries: 300,
14 ballistae: 20,
15 speed: '30 km/day',
16 supplies: {
17 food: '30 days',
18 water: '25 days',
19 wine: '100 amphorae'
20 }
21 };
22 }
23
24 return basicStatus;
25 }
26}

Custom Validation Pipes

Tribute Type Validation Pipe

Checks whether the tribute type is allowed:

1@Injectable()
2export class TributeTypeValidationPipe implements PipeTransform {
3 private readonly allowedTypes = [
4 'gold',
5 'silver',
6 'jewels',
7 'artifacts',
8 'scrolls',
9 'armor'
10 ];
11
12 transform(value: string, metadata: ArgumentMetadata) {
13 if (!value) {
14 throw new BadRequestException('Tribute type is required!');
15 }
16
17 const normalizedType = value.toLowerCase().trim();
18
19 if (!this.allowedTypes.includes(normalizedType)) {
20 throw new BadRequestException(
21 `Unknown tribute type: ${value}. Allowed types: ${this.allowedTypes.join(', ')}`
22 );
23 }
24
25 return normalizedType;
26 }
27}
28
29@Controller('tributes')
30export class TributesController {
31 @Get('type/:type')
32 getTributesByType(
33 @Param('type', TributeTypeValidationPipe) type: string
34 ) {
35 return {
36 type,
37 tributes: this.getTributesOfType(type)
38 };
39 }
40}

Legionary Rank Validation Pipe

Validates and normalizes legionary ranks:

1enum LegionaryRank {
2 MILES = 'miles',
3 TESSERARIUS = 'tesserarius',
4 OPTIO = 'optio',
5 CENTURION = 'centurion',
6 TRIBUNE = 'trybun'
7}
8
9@Injectable()
10export class LegionaryRankPipe implements PipeTransform<string, LegionaryRank> {
11 transform(value: string, metadata: ArgumentMetadata): LegionaryRank {
12 if (!value) {
13 throw new BadRequestException('Legionary rank is required!');
14 }
15
16 const normalizedRank = value.toLowerCase().replace(/s+/g, '_');
17 const rankMapping = {
18 'miles': LegionaryRank.MILES,
19 'legionary': LegionaryRank.MILES,
20 'tesserarius': LegionaryRank.TESSERARIUS,
21 'guard': LegionaryRank.TESSERARIUS,
22 'optio': LegionaryRank.OPTIO,
23 'deputy': LegionaryRank.OPTIO,
24 'centurion': LegionaryRank.CENTURION,
25 'commander': LegionaryRank.CENTURION,
26 'tribune': LegionaryRank.TRIBUNE,
27 'trybun': LegionaryRank.TRIBUNE
28 };
29
30 const rank = rankMapping[normalizedRank];
31
32 if (!rank) {
33 throw new BadRequestException(
34 `Unknown rank: ${value}. Available ranks: ${Object.values(LegionaryRank).join(', ')}`
35 );
36 }
37
38 return rank;
39 }
40}

File Upload Validation Pipe

Validates uploaded files:

1@Injectable()
2export class FileValidationPipe implements PipeTransform {
3 constructor(
4 private readonly maxSize: number = 5 * 1024 * 1024, // 5MB
5 private readonly allowedTypes: string[] = ['image/jpeg', 'image/png', 'application/pdf']
6 ) {}
7
8 transform(file: Express.Multer.File, metadata: ArgumentMetadata) {
9 if (!file) {
10 throw new BadRequestException('No file found! Where is the tribute map? ');
11 }
12
13 // Check size
14 if (file.size > this.maxSize) {
15 throw new BadRequestException(
16 `File too large! Maximum size: ${this.maxSize / 1024 / 1024}MB`
17 );
18 }
19
20 // Check type
21 if (!this.allowedTypes.includes(file.mimetype)) {
22 throw new BadRequestException(
23 `Unsupported file type: ${file.mimetype}. Allowed: ${this.allowedTypes.join(', ')}`
24 );
25 }
26
27 // Check file name
28 if (!/^[a-zA-Z0-9._-]+$/.test(file.originalname)) {
29 throw new BadRequestException('File name contains forbidden characters!');
30 }
31
32 return {
33 ...file,
34 validated: true,
35 validatedAt: new Date().toISOString(),
36 legionApproved: true
37 };
38 }
39}
40
41@Controller('maps')
42export class MapController {
43 @Post('upload')
44 @UseInterceptors(FileInterceptor('map'))
45 uploadTributeMap(
46 @UploadedFile(FileValidationPipe) file: Express.Multer.File
47 ) {
48 return {
49 message: 'Tribute map uploaded successfully!',
50 file: {
51 name: file.originalname,
52 size: file.size,
53 type: file.mimetype
54 }
55 };
56 }
57}

Transform Pipes

Coordinate Transformation Pipe

Transforms geographic coordinates:

1interface Coordinates {
2 latitude: number;
3 longitude: number;
4 formatted: string;
5}
6
7@Injectable()
8export class CoordinateTransformPipe implements PipeTransform<string, Coordinates> {
9 transform(value: string, metadata: ArgumentMetadata): Coordinates {
10 if (!value) {
11 throw new BadRequestException('Coordinates are required for navigation!');
12 }
13
14 // Support for different formats: "25.5,-80.2" or "25°30'N,80°12'W"
15 let latitude: number;
16 let longitude: number;
17
18 if (value.includes('°')) {
19 const result = this.parseDMSFormat(value);
20 latitude = result.lat;
21 longitude = result.lng;
22 } else {
23 const [lat, lng] = value.split(',').map(coord => parseFloat(coord.trim()));
24
25 if (isNaN(lat) || isNaN(lng)) {
26 throw new BadRequestException('Invalid coordinate format!');
27 }
28
29 latitude = lat;
30 longitude = lng;
31 }
32
33 // Range validation
34 if (latitude < -90 || latitude > 90) {
35 throw new BadRequestException('Latitude must be between -90 and 90');
36 }
37
38 if (longitude < -180 || longitude > 180) {
39 throw new BadRequestException('Longitude must be between -180 and 180');
40 }
41
42 return {
43 latitude,
44 longitude,
45 formatted: this.formatCoordinates(latitude, longitude)
46 };
47 }
48
49 private parseDMSFormat(value: string) {
50 // Example: "25°30'12\"N,80°15'30\"W"
51 const regex = /(\d+)°(\d+)'(\d+)"([NSEW])/g;
52 const matches = Array.from(value.matchAll(regex));
53
54 if (matches.length !== 2) {
55 throw new BadRequestException('Invalid DMS format!');
56 }
57
58 const lat = this.dmsToDecimal(matches[0]);
59 const lng = this.dmsToDecimal(matches[1]);
60
61 return { lat, lng };
62 }
63
64 private dmsToDecimal(match: RegExpMatchArray): number {
65 const degrees = parseInt(match[1]);
66 const minutes = parseInt(match[2]);
67 const seconds = parseInt(match[3]);
68 const direction = match[4];
69
70 let decimal = degrees + minutes/60 + seconds/3600;
71
72 if (direction === 'S' || direction === 'W') {
73 decimal = -decimal;
74 }
75
76 return decimal;
77 }
78
79 private formatCoordinates(lat: number, lng: number): string {
80 const latDir = lat >= 0 ? 'N' : 'S';
81 const lngDir = lng >= 0 ? 'E' : 'W';
82
83 return `${Math.abs(lat).toFixed(6)}°${latDir}, ${Math.abs(lng).toFixed(6)}°${lngDir}`
1 }
2}

Date Range Pipe

Parses and validates date ranges:

1interface DateRange {
2 start: Date;
3 end: Date;
4 duration: number; // in days
5}
6
7@Injectable()
8export class DateRangePipe implements PipeTransform<string, DateRange> {
9 transform(value: string, metadata: ArgumentMetadata): DateRange {
10 if (!value) {
11 throw new BadRequestException('Date range is required for the expedition!');
12 }
13
14 const [startStr, endStr] = value.split(',').map(date => date.trim());
15
16 if (!startStr || !endStr) {
17 throw new BadRequestException('Provide start and end dates separated by a comma');
18 }
19
20 const start = new Date(startStr);
21 const end = new Date(endStr);
22
23 // Date validation
24 if (isNaN(start.getTime())) {
25 throw new BadRequestException(`Invalid start date: ${startStr}`);
26 }
27
28 if (isNaN(end.getTime())) {
29 throw new BadRequestException(`Invalid end date: ${endStr}`);
30 }
31
32 if (start >= end) {
33 throw new BadRequestException('Start date must be earlier than end date!');
34 }
35
36 const now = new Date();
37 if (end < now) {
38 throw new BadRequestException('Cannot plan expeditions in the past!');
39 }
40
41 const duration = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
42
43 if (duration > 365) {
44 throw new BadRequestException('Expeditions longer than a year are too risky!');
45 }
46
47 return { start, end, duration };
48 }
49}
50
51@Controller('expeditions')
52export class ExpeditionController {
53 @Get('plan')
54 planExpedition(
55 @Query('dates', DateRangePipe) dateRange: DateRange,
56 @Query('destination', CoordinateTransformPipe) destination: Coordinates
57 ) {
58 return {
59 expedition: {
60 start: dateRange.start,
61 end: dateRange.end,
62 duration: `${dateRange.duration} days`,
63 destination: destination.formatted
64 },
65 supplies: this.calculateSupplies(dateRange.duration),
66 legionaries: this.calculateLegionSize(dateRange.duration)
67 };
68 }
69}

Array Validation Pipe

1@Injectable()
2export class LegionArrayPipe implements PipeTransform {
3 transform(value: any, metadata: ArgumentMetadata) {
4 if (!Array.isArray(value)) {
5 throw new BadRequestException('The legionary list must be an array!');
6 }
7
8 if (value.length === 0) {
9 throw new BadRequestException('The legion cannot be empty! Who will march?');
10 }
11
12 if (value.length > 300) {
13 throw new BadRequestException('Legion too large! The fort cannot hold more than 300 legionaries!');
14 }
15
16 // Validate each legionary
17 const validatedLegion = value.map((member, index) => {
18 if (!member.name || typeof member.name !== 'string') {
19 throw new BadRequestException(`Legionary #${index + 1} must have a name!`);
20 }
21
22 if (!member.rank || !Object.values(LegionaryRank).includes(member.rank)) {
23 throw new BadRequestException(`Legionary ${member.name} has an invalid rank!`);
24 }
25
26 return {
27 ...member,
28 name: member.name.trim(),
29 joinedAt: new Date().toISOString(),
30 legionaryId: `legionary-${Date.now()}-${index}`
31 };
32 });
33
34 // Check if there is a centurion
35 const hasCenturion = validatedLegion.some(member =>
36 [LegionaryRank.CENTURION, LegionaryRank.TRIBUNE].includes(member.rank)
37 );
38
39 if (!hasCenturion) {
40 throw new BadRequestException('The legion must have a centurion!');
41 }
42
43 return validatedLegion;
44 }
45}

Optional Validation Pipe

1@Injectable()
2export class OptionalValidationPipe implements PipeTransform {
3 constructor(private readonly innerPipe: PipeTransform) {}
4
5 transform(value: any, metadata: ArgumentMetadata) {
6 if (value === undefined || value === null || value === '') {
7 return undefined;
8 }
9
10 return this.innerPipe.transform(value, metadata);
11 }
12}
13
14// Usage
15@Controller('search')
16export class SearchController {
17 @Get('tributes')
18 searchTributes(
19 @Query('type', new OptionalValidationPipe(new TributeTypeValidationPipe()))
20 type?: string,
21 @Query('minValue', new OptionalValidationPipe(ParseIntPipe))
22 minValue?: number
23 ) {
24 return {
25 filters: { type, minValue },
26 results: this.searchWithFilters(type, minValue)
27 };
28 }
29}

Global Pipes

1// main.ts
2async function bootstrap() {
3 const app = await NestFactory.create(AppModule);
4
5 app.useGlobalPipes(
6 new ValidationPipe({
7 whitelist: true,
8 forbidNonWhitelisted: true,
9 transform: true,
10 disableErrorMessages: false,
11 })
12 );
13
14 await app.listen(3000);
15}
16
17// Or in a module
18@Module({
19 providers: [
20 {
21 provide: APP_PIPE,
22 useClass: ValidationPipe,
23 },
24 ],
25})
26export class AppModule {}
Go to CodeWorlds