Skip to content

NestJS Custom Pipes

Sometimes a domain-specific rule doesn't fit any built-in pipe, like checking that a slug is unique, or converting a comma-separated string into an array. This lesson builds a custom pipe from scratch.

Building a Custom Pipe

A custom pipe implements PipeTransform<TInput, TOutput>, giving you full control over both the accepted input type and the value ultimately passed to the handler.

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

@Injectable()
export class ParseCsvPipe implements PipeTransform<string, string[]> {
  transform(value: string): string[] {
    if (!value) return [];
    return value.split(',').map((item) => item.trim());
  }
}

This pipe converts a comma-separated query string like ?tags=cat,pet,orange into a clean array of trimmed strings before it reaches the handler.

A Pipe That Depends on a Service

@Injectable()
export class UniqueSlugPipe implements PipeTransform {
  constructor(private readonly catsService: CatsService) {}

  async transform(value: string) {
    const exists = await this.catsService.slugExists(value);
    if (exists) throw new BadRequestException('Slug already in use');
    return value;
  }
}
  • Because custom pipes are @Injectable(), they can inject services just like any other provider.
  • Pipes can be asynchronous, Nest awaits the returned promise before calling the handler.
  • Throwing inside a custom pipe automatically produces the corresponding HTTP error response.
  • Custom pipes are typically applied per-parameter, close to where the transformation is needed.

Custom Pipe Cheatsheet

The essentials for writing a domain-specific pipe.

Piece Purpose
implements PipeTransform<In, Out> Declares the pipe's input and output types
transform(value, metadata) Performs the conversion or validation
ArgumentMetadata Describes the parameter being processed
Throwing an exception Rejects the request with the matching HTTP status

When Logic Belongs in a Pipe vs. a Service

A pipe is the right place for logic that's purely about shaping or validating one specific argument. Business rules spanning multiple fields or requiring broader context are usually clearer inside the service method itself, where the full picture of the operation is visible.

  • Good pipe candidate: converting a query string into a typed array.
  • Good pipe candidate: rejecting a malformed identifier before it reaches the service.
  • Better as service logic: checking a multi-step business invariant across several fields.

Testing Custom Pipes in Isolation

Because a pipe is just a class with a transform() method, testing it requires no HTTP setup at all, instantiate it directly (injecting mocks for any dependencies) and call transform() with sample input.

Common Mistakes

  • Putting multi-field business rules inside a pipe meant to validate a single argument.
  • Forgetting @Injectable() on a pipe that needs constructor-injected dependencies.
  • Not handling the undefined/empty case explicitly, causing runtime errors on optional parameters.
  • Writing pipe logic that's hard to unit test because it reaches for global state instead of injected dependencies.

Key Takeaways

  • Custom pipes implement PipeTransform for domain-specific validation or conversion needs.
  • Pipes are @Injectable(), so they support dependency injection like any other provider.
  • Async pipes are supported and awaited automatically by Nest.
  • Pipes are best kept focused on a single argument's shape, not broader business rules.

Pro Tip

Before writing a new custom pipe, ask whether the same transformation could instead be expressed as a class-transformer decorator on the DTO itself (like @Transform()), for many cases that keeps the logic colocated with the field it affects rather than in a separate pipe class.