Custom Pipes

Implementing PipeTransform

A custom pipe is an @Injectable() class with a transform(value, metadata) method. ShopEase uses URL slugs like /products/slug/boat-airdopes-141; this pipe normalises and checks them.

src/common/pipes/slug.pipe.ts

import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common';

@Injectable()
export class SlugPipe implements PipeTransform<string, string> {
  transform(value: string, metadata: ArgumentMetadata): string {
    const slug = value.trim().toLowerCase();
    if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug)) {
      throw new BadRequestException(`${metadata.data} must be a valid slug (a-z, 0-9 and hyphens)`);
    }
    return slug;
  }
}

// usage:  @Get('slug/:slug') bySlug(@Param('slug', SlugPipe) slug: string) {}

A Pipe that Loads an Entity

A pipe can inject services. This one turns an id into the actual product, or a 404:

src/products/pipes/product-by-id.pipe.ts

import { Injectable, NotFoundException, PipeTransform } from '@nestjs/common';
import { ProductsService } from '../products.service';
import { Product } from '../entities/product.entity';

@Injectable()
export class ProductByIdPipe implements PipeTransform<string, Promise<Product>> {
  constructor(private readonly products: ProductsService) {}

  async transform(value: string): Promise<Product> {
    const product = await this.products.findById(Number(value));
    if (!product) throw new NotFoundException(`Product ${value} not found`);
    return product;
  }
}

// usage:  @Get(':id') findOne(@Param('id', ProductByIdPipe) product: Product) { return product; }

Exercise: Write a TrimPipe that trims every string property of a body object (one level deep) and leaves other values unchanged.

Show answer
import { Injectable, PipeTransform } from '@nestjs/common';

@Injectable()
export class TrimPipe implements PipeTransform {
  transform(value: unknown) {
    if (value === null || typeof value !== 'object' || Array.isArray(value)) return value;
    return Object.fromEntries(
      Object.entries(value).map(([k, v]) => [k, typeof v === 'string' ? v.trim() : v]),
    );
  }
}