Custom Pipes & Advanced Validation

Implement PipeTransform for domain-specific parsing, and drive schema validation (Zod/Joi) through a pipe.

Built-in pipes cover primitives, but real apps validate domain shapes: a valid slug, a well-formed cursor, a body matching a Zod schema. A custom pipe is a tiny class implementing PipeTransform.

The contract

transform(value, metadata) receives the incoming value and its ArgumentMetadata (type, metatype, param name). Return the transformed value, or throw a BadRequestException to reject it:

@Injectable()
export class TrimPipe implements PipeTransform {
  transform(value: unknown) {
    return typeof value === 'string' ? value.trim() : value;
  }
}

Schema validation through a pipe

The most common senior pattern is wrapping a schema validator (Zod, Joi, Yup) in a pipe. The pipe parses the body, and on failure maps the validator's error into a clean 400. This gives you runtime type-safety that class-validator's decorators can't always express - discriminated unions, refinements, transforms.

Where to bind it

  • Per parameter: @Body(new ZodValidationPipe(schema)) - most explicit.
  • Per handler/controller: @UsePipes(...).
  • Globally: app.useGlobalPipes(...) for cross-cutting rules.

Pipes vs guards vs interceptors

Keep the boundaries clean: a pipe shapes and validates a single argument, a guard decides access, an interceptor wraps the whole handler. When a pipe starts making auth decisions, it's in the wrong layer.

Example

Example Β· typescript
import {
  PipeTransform,
  Injectable,
  ArgumentMetadata,
  BadRequestException,
} from '@nestjs/common';
import { z, ZodSchema } from 'zod';

// One reusable pipe for ANY Zod schema.
@Injectable()
export class ZodValidationPipe<T> implements PipeTransform<unknown, T> {
  constructor(private readonly schema: ZodSchema<T>) {}

  transform(value: unknown, _meta: ArgumentMetadata): T {
    const result = this.schema.safeParse(value);
    if (!result.success) {
      throw new BadRequestException({
        message: 'Validation failed',
        issues: result.error.issues.map((i) => ({
          path: i.path.join('.'),
          error: i.message,
        })),
      });
    }
    return result.data;
  }
}

// Schema is the single source of truth for shape AND type.
export const CreateOrderSchema = z
  .object({
    items: z.array(z.string().min(1)).nonempty(),
    coupon: z.string().regex(/^[A-Z0-9]{4,10}$/).optional(),
    priority: z.enum(['standard', 'express']).default('standard'),
  })
  .strict();

export type CreateOrderDto = z.infer<typeof CreateOrderSchema>;

// Usage in a controller:
// @Post()
// create(
//   @Body(new ZodValidationPipe(CreateOrderSchema)) dto: CreateOrderDto,
// ) {
//   return this.orders.create(dto);
// }

When to use it

  • A developer writes a ZodPipe that validates the request body against a Zod schema and throws BadRequestException with the formatted Zod error.
  • A team creates a ParseObjectIdPipe that validates a route parameter is a valid MongoDB ObjectId and converts it to an ObjectId instance.
  • An engineer builds a SanitizePipe that runs the input through DOMPurify before saving user-generated HTML content to the database.

More examples

Zod schema validation pipe

A generic pipe that accepts any Zod schema and throws a formatted BadRequestException when the value fails validation.

Example Β· ts
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
import { ZodSchema } from 'zod';

@Injectable()
export class ZodPipe implements PipeTransform {
  constructor(private schema: ZodSchema) {}

  transform(value: unknown) {
    const result = this.schema.safeParse(value);
    if (!result.success) {
      throw new BadRequestException(result.error.format());
    }
    return result.data;
  }
}

// Usage: @Body(new ZodPipe(createProductSchema)) dto: CreateProductDto

MongoDB ObjectId validation pipe

Validates and converts a route parameter string to a Mongoose ObjectId, returning 400 for invalid ID strings.

Example Β· ts
import { PipeTransform, Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
import { Types } from 'mongoose';

@Injectable()
export class ParseObjectIdPipe implements PipeTransform<string, Types.ObjectId> {
  transform(value: string): Types.ObjectId {
    if (!Types.ObjectId.isValid(value)) {
      throw new BadRequestException(`'${value}' is not a valid ObjectId`);
    }
    return new Types.ObjectId(value);
  }
}

Joi schema validation pipe

Runs the value against a Joi schema and collects all validation errors in a single pass using abortEarly: false before throwing.

Example Β· ts
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
import * as Joi from 'joi';

@Injectable()
export class JoiPipe implements PipeTransform {
  constructor(private schema: Joi.Schema) {}

  transform(value: unknown) {
    const { error, value: validated } = this.schema.validate(value, {
      abortEarly: false,
    });
    if (error) {
      throw new BadRequestException(error.details.map(d => d.message));
    }
    return validated;
  }
}

Discussion

  • Be the first to comment on this lesson.