Chapter 07 — Pipes (Transform & Validate)

Chapter 07 — Pipes (Transform & Validate)

Hey everyone! Welcome back to Namaste NestJS! 🙏

In the last chapter you switched on the ValidationPipe and watched bad data bounce off your API. Today we zoom out and understand the whole family that pipe belongs to. A Pipe is a checkpoint that sits right before your handler and does one of two jobs: transform the input, or validate it. Simple idea, huge payoff.

What we will cover:

  • What a pipe is and its two jobs
  • Where pipes sit in the request pipeline
  • Built-in pipes (ParseIntPipe, ParseUUIDPipe...)
  • Binding pipes at param, handler, and global level
  • Passing options to a pipe
  • Writing your own custom pipe
  • Transform vs validation pipes
  • Common mistakes
  • Interview Questions

1. What is a Pipe?

┌─────────────────────────────────────────────────────────────┐
│                       WHAT IS A PIPE?                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   A PIPE runs JUST BEFORE your handler and does one of:    │
│                                                             │
│   1. TRANSFORM  → change input into the desired form        │
│                   e.g. "42" (string) → 42 (number)          │
│                                                             │
│   2. VALIDATE   → check input; throw if it's invalid        │
│                   e.g. reject a body that fails the DTO      │
│                                                             │
│   Input ──→ [ PIPE ] ──→ handler receives clean data        │
│                 │                                           │
│           invalid? → throws → 400 (handler never runs)     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

A simple analogy: a pipe is airport security. Everything passing through is either checked (validation) or repackaged (transformation) before it's allowed to the gate (your handler). Nothing dodgy gets past.


2. Where Pipes Sit in the Pipeline

Order matters. Pipes run after guards and interceptors have let the request through, but right before your controller method gets its arguments.

THE REQUEST PIPELINE (simplified — full version in Deep Dive 02):
──────────────────────────────────────────────────────────────────
  Request
     │
     ▼
  Middleware  →  Guards  →  Interceptors(pre)  →  ★ PIPES ★  →  HANDLER
                                                     │
                                          transform / validate the
                                          @Param, @Query, @Body args

In simple words: by the time your method runs, pipes have already cleaned and coerced every argument. That's why your handler can just trust its inputs.


3. Built-in Pipes

Nest ships with ready-made pipes for the most common transform/validate jobs:

PipeJob
ParseIntPipeConvert string → integer (throws if not numeric)
ParseFloatPipeConvert string → float
ParseBoolPipeConvert "true"/"false" → boolean
ParseUUIDPipeValidate the value is a UUID
ParseArrayPipeParse/validate an array
ParseEnumPipeValidate against an enum
ValidationPipeValidate a whole DTO (Chapter 06)
DefaultValuePipeSupply a fallback when a value is missing

This solves the "route params are always strings" problem from Chapter 03:

import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';

@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {  // 👈 id arrives as a real number
  // no more manual Number(id) — and "abc" is auto-rejected with 400
  return this.catsService.findOne(id);
}
HAND TRACE:  GET /cats/abc   (ParseIntPipe on :id)
────────────────────────────────────────────────────
  1. id extracted as the string "abc"
  2. ParseIntPipe tries Number("abc") → NaN
  3. Not a valid integer → pipe THROWS
                     ▼
     Response: 400  "Validation failed (numeric string is expected)" ✔
     (findOne never runs)

4. Where to Bind a Pipe

You can attach a pipe at three levels, from most local to most global:

1. PARAMETER level  → just this one argument
   @Param('id', ParseIntPipe) id: number

2. HANDLER level    → all args of this method
   @UsePipes(new ValidationPipe())
   @Post() create(@Body() dto: CreateCatDto) {}

3. GLOBAL level     → the whole app (set in main.ts)
   app.useGlobalPipes(new ValidationPipe());
RULE OF THUMB:
──────────────
  • Validation of DTOs  → GLOBAL (one line, everywhere)
  • Parsing a single param (ParseIntPipe) → PARAMETER level

5. Passing Options to a Pipe

When you need to configure a pipe, pass an instance instead of the class:

// class form (default options):
@Param('id', ParseIntPipe) id: number

// instance form (custom options — e.g. a specific error status):
@Param('id', new ParseIntPipe({ errorHttpStatusCode: 406 })) id: number

Q: When do I pass the class vs a new instance?

A: Pass the class (ParseIntPipe) when the defaults are fine — Nest instantiates it for you and can inject its dependencies. Pass an instance (new ParseIntPipe({...})) when you need to configure options.


6. Writing Your Own Custom Pipe

A custom pipe is a class implementing PipeTransform. It has one method, transform(value, metadata), which returns the (possibly transformed) value or throws.

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

@Injectable()
export class ParsePositivePipe implements PipeTransform {
  transform(value: any) {
    const num = Number(value);

    if (isNaN(num) || num <= 0) {
      throw new BadRequestException('Value must be a positive number');  // ❌ reject
    }

    return num;   // ✅ transformed & validated value handed to the handler
  }
}

Use it exactly like a built-in pipe:

@Get(':id')
findOne(@Param('id', ParsePositivePipe) id: number) {
  return this.catsService.findOne(id);   // id is guaranteed positive here
}

Note: whatever transform() returns becomes the value your handler receives. That's how a pipe both cleans and reshapes input in one step.


7. Transform vs Validation Pipes

┌─────────────────────────────────────────────────────────────┐
│   TWO FLAVORS OF PIPE                                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   TRANSFORM PIPE                VALIDATION PIPE             │
│   ──────────────                ───────────────            │
│   changes the value             checks the value           │
│   returns new value             returns value unchanged    │
│                                 (or throws)                │
│   e.g. ParseIntPipe             e.g. ValidationPipe         │
│        "42" → 42                     bad DTO → 400          │
│                                                             │
│   Many pipes do BOTH (validate, then return coerced value).│
│                                                             │
└─────────────────────────────────────────────────────────────┘

8. Little Traps Beginners Fall Into

┌─────────────────────────────────────────────────────────────┐
│   COMMON MISTAKES ❌                                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Forgetting a pipe RETURNS the value it wants the handler   │
│  to get. Return the transformed value, not the raw one.    │
│                                                             │
│  Putting heavy business logic in a pipe.                    │
│     → Pipes are for transform/validate only. Logic → service│
│                                                             │
│  Expecting a param-level ParseIntPipe to validate a body.  │
│     → Use ValidationPipe + a DTO for bodies.               │
│                                                             │
│  Throwing a generic Error instead of an HttpException.     │
│     → Throw BadRequestException so the client gets a 400.  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Interview Questions — Quick Fire!

Q: What is a pipe in NestJS?

"A pipe is a class that runs just before a route handler and either transforms the input into a desired form or validates it, throwing an exception if it's invalid. Nest provides built-in pipes like ParseIntPipe and ValidationPipe, and you can write custom ones by implementing the PipeTransform interface."

Q: What are the two main use cases for pipes?

"Transformation — converting input data, for example a numeric string in a URL into an actual number; and validation — checking that input meets certain rules and rejecting it with a 400 if not. A single pipe can do both: validate, then return the coerced value."

Q: At what levels can you bind a pipe?

"At the parameter level for a single argument, at the handler level with @UsePipes for all of a method's arguments, and globally with app.useGlobalPipes for the entire application. DTO validation is usually applied globally, while parsing pipes like ParseIntPipe are applied at the parameter level."

Q: How do you write a custom pipe?

"You create a class decorated with @Injectable that implements PipeTransform and its transform method. Inside transform you inspect the value, throw a BadRequestException if it's invalid, or return the transformed value, which then becomes the argument the handler receives."

Q: Where do pipes run relative to guards and the handler?

"Pipes run after middleware, guards, and the pre-controller interceptor logic, but immediately before the handler executes. By the time the handler runs, all of its arguments have already been transformed and validated by the pipes."


Key Points to Remember

ConceptKey Takeaway
PipeRuns before the handler; transforms or validates input.
Built-in pipesParseIntPipe, ParseUUIDPipe, ValidationPipe, DefaultValuePipe...
Binding levelsParameter · handler (@UsePipes) · global.
Custom pipeimplements PipeTransform; transform() returns or throws.
Returns valueWhatever transform() returns becomes the handler's argument.
Throw HttpExceptionUse BadRequestException so clients get a proper 400.

What's Next?

Pipes handle the data arguments. But some work needs to happen even earlier — before routing, on the raw request itself: logging, adding headers, parsing cookies. That's the job of Middleware, the oldest checkpoint in the Express world. Chapter 08 shows how Nest adopts it and where it fits in the pipeline.

Keep coding, keep shipping! See you in the next one!