Bird
Raised Fist0
NestJSframework~15 mins

Custom pipes in NestJS - Deep Dive

Choose your learning style10 modes available
Overview - Custom pipes
What is it?
Custom pipes in NestJS are special classes that transform or validate data before it reaches your route handlers. They act like filters that check or change incoming data, such as request parameters or body content. This helps keep your code clean and safe by handling data issues early. Pipes run automatically during the request lifecycle.
Why it matters
Without custom pipes, you would have to manually check and change data inside every route handler, which is repetitive and error-prone. Pipes let you centralize this logic, making your app more reliable and easier to maintain. They prevent bugs caused by bad data and improve user experience by catching errors early.
Where it fits
Before learning custom pipes, you should understand basic NestJS controllers and how requests flow through them. After mastering pipes, you can explore advanced topics like guards, interceptors, and exception filters to build robust APIs.
Mental Model
Core Idea
Custom pipes are like gatekeepers that check and prepare data before it enters your app's main logic.
Think of it like...
Imagine a security guard at a building entrance who checks visitors' IDs and only lets in those who meet the rules. Pipes do the same for data, allowing only valid and well-formed information to pass through.
Request Data
   ↓
┌───────────────┐
│   Custom Pipe │  <-- checks or changes data
└───────────────┘
   ↓
Route Handler (controller method)
Build-Up - 7 Steps
1
FoundationWhat are Pipes in NestJS
🤔
Concept: Introduce the basic idea of pipes as data transformers and validators in NestJS.
In NestJS, pipes are classes that implement the PipeTransform interface. They have a transform() method that receives input data and returns transformed data or throws an error if validation fails. Pipes run before route handlers to prepare or check data.
Result
You understand that pipes act on incoming data automatically before your controller code runs.
Knowing that pipes run before your main code helps you see how they keep your handlers clean and focused on business logic.
2
FoundationCreating a Simple Custom Pipe
🤔
Concept: Learn how to write a basic custom pipe class with transform logic.
Create a class that implements PipeTransform and add a transform(value, metadata) method. For example, a pipe that converts a string to uppercase: class UppercasePipe implements PipeTransform { transform(value: any) { return typeof value === 'string' ? value.toUpperCase() : value; } } This pipe changes input strings to uppercase before the handler receives them.
Result
You can write a pipe that modifies data as it passes through.
Understanding the transform method is key because it controls how data changes or gets validated.
3
IntermediateUsing Pipes for Validation
🤔Before reading on: do you think pipes can only change data, or can they also stop bad data from reaching handlers? Commit to your answer.
Concept: Pipes can also check data and throw errors to stop invalid input.
You can throw exceptions inside transform() to reject bad data. For example, a pipe that ensures a number is positive: import { PipeTransform, BadRequestException } from '@nestjs/common'; class PositiveIntPipe implements PipeTransform { transform(value: any) { const val = parseInt(value, 10); if (isNaN(val) || val <= 0) { throw new BadRequestException('Value must be a positive integer'); } return val; } } This pipe stops the request if the input is invalid.
Result
Invalid data causes an error response before your handler runs.
Knowing pipes can reject data early helps prevent bugs and keeps your app safe.
4
IntermediateApplying Pipes at Different Levels
🤔Before reading on: do you think pipes can be applied only to single parameters, or also to entire routes or globally? Commit to your answer.
Concept: Pipes can be used on method parameters, entire route handlers, or globally for all routes.
You can attach pipes: - To a single parameter: @Param('id', new PositiveIntPipe()) - To a whole route: @UsePipes(new ValidationPipe()) - Globally in main.ts: app.useGlobalPipes(new ValidationPipe()) This flexibility lets you control where and how data is checked or transformed.
Result
You can choose the scope of pipes to balance performance and code clarity.
Understanding pipe scope helps you organize validation logic efficiently.
5
AdvancedCombining Multiple Pipes Together
🤔Before reading on: do you think multiple pipes run in sequence or all at once? Commit to your answer.
Concept: Multiple pipes can be stacked and run one after another on the same data.
You can pass an array of pipes to @UsePipes or parameter decorators: @UsePipes(new TrimPipe(), new UppercasePipe()) Each pipe runs in order, passing its output to the next. This lets you build complex transformations and validations step-by-step.
Result
Data flows through a pipeline of transformations before reaching your handler.
Knowing pipes chain in sequence helps you design modular and reusable data processing.
6
AdvancedAccessing Metadata in Pipes
🤔
Concept: Pipes receive metadata about the data they process, enabling context-aware logic.
The transform method gets a second argument with info like the type of data (body, param, query) and the parameter name: transform(value: any, metadata: ArgumentMetadata) { console.log(metadata.type); // 'body', 'param', or 'query' console.log(metadata.data); // parameter name return value; } This helps pipes behave differently depending on where they are used.
Result
You can write smarter pipes that adapt to their context.
Understanding metadata lets you create flexible pipes that work well in many situations.
7
ExpertPerformance and Error Handling in Pipes
🤔Before reading on: do you think throwing errors in pipes affects app performance or error clarity? Commit to your answer.
Concept: Throwing exceptions in pipes triggers NestJS error handling, but excessive or complex pipes can impact performance and error clarity.
Pipes run synchronously or asynchronously. Async pipes return Promises and can handle async validation. However, many pipes or heavy logic can slow requests. Also, error messages should be clear to help users and developers. Use built-in exceptions like BadRequestException for standard HTTP errors. Example async pipe: async transform(value: any) { const valid = await someAsyncCheck(value); if (!valid) throw new BadRequestException('Invalid data'); return value; } Use global pipes carefully to avoid overhead on every request.
Result
You balance validation thoroughness with app speed and user clarity.
Knowing pipe performance and error handling tradeoffs helps build scalable, user-friendly APIs.
Under the Hood
NestJS pipes are classes that implement the PipeTransform interface. When a request comes in, NestJS calls the transform() method of each pipe in the order they are applied. If transform returns a value, that value replaces the original input for the next pipe or the handler. If transform throws an exception, NestJS stops processing and sends an error response. Pipes can be synchronous or asynchronous, supporting Promises. NestJS uses metadata to pass context about the data being processed, enabling pipes to adapt their behavior.
Why designed this way?
Pipes were designed to separate data validation and transformation from business logic, following the single responsibility principle. This design keeps controllers clean and reusable. The interface-based approach allows developers to create custom pipes easily. Supporting both sync and async pipes enables integration with external services or databases during validation. The metadata parameter provides flexibility without complicating the pipe interface.
Incoming Request
      ↓
┌─────────────────────┐
│ NestJS Framework    │
│  Calls Pipes in     │
│  order applied      │
└─────────┬───────────┘
          ↓
┌─────────────────────┐
│ Pipe 1 transform()   │
│  (sync or async)    │
└─────────┬───────────┘
          ↓
┌─────────────────────┐
│ Pipe 2 transform()   │
│  (sync or async)    │
└─────────┬───────────┘
          ↓
┌─────────────────────┐
│ Route Handler       │
│ Receives final data │
└─────────────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Do pipes only transform data, or can they also reject requests? Commit to your answer.
Common Belief:Pipes are only for changing data, not for validation or rejecting bad input.
Tap to reveal reality
Reality:Pipes can both transform data and throw exceptions to reject invalid input before it reaches handlers.
Why it matters:Thinking pipes only transform data leads to missing early validation, causing bugs and security risks.
Quick: Can you apply a pipe globally to affect all routes? Commit to yes or no.
Common Belief:Pipes can only be used on individual parameters or routes, not globally.
Tap to reveal reality
Reality:NestJS allows global pipes that apply to every incoming request, simplifying app-wide validation.
Why it matters:Missing global pipes causes duplicated validation code and inconsistent data checks.
Quick: Do multiple pipes run in parallel or in sequence? Commit to your answer.
Common Belief:Multiple pipes run at the same time on the same data.
Tap to reveal reality
Reality:Pipes run one after another in the order they are applied, each receiving the previous pipe's output.
Why it matters:Misunderstanding pipe order can cause unexpected data states and bugs.
Quick: Are pipes always synchronous? Commit to yes or no.
Common Belief:Pipes must be synchronous and cannot handle async operations.
Tap to reveal reality
Reality:Pipes can be asynchronous, returning Promises to handle async validation or transformation.
Why it matters:Assuming pipes are sync limits their power and prevents integration with async services.
Expert Zone
1
Custom pipes can leverage dependency injection to access services, enabling complex validation like database lookups.
2
The order of pipes matters deeply; a pipe that transforms data must run before a pipe that validates it to avoid errors.
3
Global pipes affect every request, so heavy logic here can degrade performance; use selectively or cache results.
When NOT to use
Avoid using pipes for business logic or side effects like logging or database writes. Instead, use interceptors or services. Also, do not use pipes for response transformation; use interceptors for that. For complex validation involving multiple fields, consider using class-validator decorators with ValidationPipe instead of custom pipes.
Production Patterns
In production, developers often combine built-in ValidationPipe with class-validator for schema validation, and add custom pipes for special cases like parsing IDs or sanitizing inputs. Global pipes enforce app-wide rules, while route-level pipes handle specific needs. Async pipes validate tokens or permissions by calling external services before allowing access.
Connections
Middleware
Both pipes and middleware process requests, but middleware runs earlier and can modify requests or responses globally, while pipes focus on data transformation and validation at the controller level.
Understanding middleware helps clarify where pipes fit in the request lifecycle and why pipes are better for fine-grained data checks.
Functional Programming - Function Composition
Pipes chain transformations in sequence, similar to composing functions where output of one is input to the next.
Seeing pipes as composed functions helps grasp how data flows and transforms step-by-step.
Airport Security Screening
Like airport security checks passengers step-by-step for safety, pipes check and prepare data step-by-step before it enters the system.
This connection shows how layered checks improve safety and reliability in complex systems.
Common Pitfalls
#1Throwing generic errors without HTTP context
Wrong approach:throw new Error('Invalid input');
Correct approach:throw new BadRequestException('Invalid input');
Root cause:Using generic errors prevents NestJS from sending proper HTTP status codes and messages.
#2Applying heavy validation logic globally without caching
Wrong approach:app.useGlobalPipes(new HeavyValidationPipe()); // runs expensive checks on every request
Correct approach:Use global pipes for lightweight validation and apply heavy pipes only on needed routes or cache results.
Root cause:Not considering performance impact of global pipes leads to slow app response times.
#3Ignoring async nature of some pipes and not awaiting
Wrong approach:transform(value) { const valid = asyncCheck(value); if (!valid) throw new BadRequestException(); return value; }
Correct approach:async transform(value) { const valid = await asyncCheck(value); if (!valid) throw new BadRequestException(); return value; }
Root cause:Forgetting to mark transform as async and await async calls causes unexpected behavior and bugs.
Key Takeaways
Custom pipes in NestJS let you cleanly transform and validate incoming data before it reaches your route handlers.
Pipes run in sequence and can be applied at parameter, route, or global levels for flexible control.
They can throw exceptions to reject bad data early, improving app safety and user experience.
Pipes support both synchronous and asynchronous operations, enabling complex validations.
Understanding pipe order, metadata, and performance impact is key to building robust, maintainable APIs.

Practice

(1/5)
1.

What is the main purpose of a custom pipe in NestJS?

easy
A. To transform or validate data before it reaches the route handler
B. To handle database connections automatically
C. To manage user sessions and cookies
D. To style the frontend components

Solution

  1. Step 1: Understand the role of pipes in NestJS

    Pipes are used to transform or validate incoming data before it reaches the controller methods.
  2. Step 2: Identify what custom pipes specifically do

    Custom pipes allow developers to create their own logic for data transformation or validation to ensure data safety and correctness.
  3. Final Answer:

    To transform or validate data before it reaches the route handler -> Option A
  4. Quick Check:

    Custom pipes = data validation/transformation [OK]
Hint: Pipes act on data before controller uses it [OK]
Common Mistakes:
  • Confusing pipes with middleware
  • Thinking pipes handle database or session logic
  • Assuming pipes style frontend components
2.

Which of the following is the correct way to define a custom pipe class in NestJS?

export class MyPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    // logic here
  }
}
easy
A. export class MyPipe implements PipeTransform { transform(value: any) { } }
B. export class MyPipe implements PipeTransform { transform(value: any, metadata: ArgumentMetadata) { } }
C. export function MyPipe(value: any) { return value; }
D. export class MyPipe { transform(value: any, metadata: ArgumentMetadata) { } }

Solution

  1. Step 1: Recall the PipeTransform interface signature

    The transform method must accept two parameters: value and metadata of type ArgumentMetadata.
  2. Step 2: Check which option matches the correct method signature

    export class MyPipe implements PipeTransform { transform(value: any, metadata: ArgumentMetadata) { } } correctly implements the transform method with both parameters and the class implements PipeTransform.
  3. Final Answer:

    export class MyPipe implements PipeTransform { transform(value: any, metadata: ArgumentMetadata) { } } -> Option B
  4. Quick Check:

    transform(value, metadata) = correct signature [OK]
Hint: transform method needs value and metadata parameters [OK]
Common Mistakes:
  • Omitting the metadata parameter
  • Using a function instead of a class
  • Not implementing PipeTransform interface
3.

Given this custom pipe code, what will be the output if the input value is "123"?

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

@Injectable()
export class ParseIntPipe implements PipeTransform {
  transform(value: any) {
    const val = parseInt(value, 10);
    if (isNaN(val)) {
      throw new BadRequestException('Validation failed');
    }
    return val;
  }
}

Input: "123"

medium
A. Returns NaN
B. Throws BadRequestException
C. Returns string "123"
D. Returns number 123

Solution

  1. Step 1: Understand parseInt behavior on string "123"

    parseInt("123", 10) converts the string "123" to the number 123.
  2. Step 2: Check the validation condition

    Since 123 is a valid number, isNaN(val) is false, so no exception is thrown and the number 123 is returned.
  3. Final Answer:

    Returns number 123 -> Option D
  4. Quick Check:

    parseInt("123") = 123 [OK]
Hint: parseInt("123") returns number 123, no error [OK]
Common Mistakes:
  • Thinking it returns string instead of number
  • Assuming it throws error for numeric strings
  • Confusing NaN with valid number
4.

Identify the error in this custom pipe code:

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

@Injectable()
export class UppercasePipe implements PipeTransform {
  transform(value: string) {
    return value.toUpperCase;
  }
}
medium
A. Missing @Injectable decorator
B. transform method missing return type
C. toUpperCase is used without parentheses
D. PipeTransform interface not implemented

Solution

  1. Step 1: Check the transform method implementation

    The method returns value.toUpperCase without parentheses, which is a reference to the function, not the result.
  2. Step 2: Identify correct usage of toUpperCase

    toUpperCase is a method and must be called with parentheses: value.toUpperCase()
  3. Final Answer:

    toUpperCase is used without parentheses -> Option C
  4. Quick Check:

    Method calls need () to execute [OK]
Hint: Remember to call string methods with () [OK]
Common Mistakes:
  • Forgetting parentheses on method calls
  • Assuming decorators are missing when present
  • Ignoring interface implementation
5.

You want to create a custom pipe that trims whitespace and converts input to lowercase. Which of these implementations correctly achieves this?

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

@Injectable()
export class NormalizePipe implements PipeTransform {
  transform(value: any) {
    // Which option is correct?
  }
}
hard
A. return value.trim().toLowerCase();
B. return value.replace(/\s/g, '').toLowerCase();
C. return value.trim.toLowerCase();
D. return value.toLowerCase.trim();

Solution

  1. Step 1: Understand method chaining order

    trim() removes whitespace from start and end, then toLowerCase() converts to lowercase. The order matters to avoid errors.
  2. Step 2: Check method call syntax

    Both trim() and toLowerCase() are methods and must be called with parentheses. return value.trim().toLowerCase(); correctly calls them in order: value.trim().toLowerCase()
  3. Final Answer:

    return value.trim().toLowerCase(); -> Option A
  4. Quick Check:

    Correct method chaining with () = return value.trim().toLowerCase(); [OK]
Hint: Call trim() before toLowerCase(), both with () [OK]
Common Mistakes:
  • Calling methods without parentheses
  • Wrong method chaining order
  • Using properties instead of methods
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard