Skip to content

Feature: Pipes & ValidationPipe (@UsePipes, PipeTransform, ParseIntPipe, etc.) #116

@ItayTheDar

Description

@ItayTheDar

Overview

PyNest has no data transformation or validation pipeline at the routing layer. Validation today is implicit — Pydantic models in function signatures are parsed by FastAPI automatically, with no opportunity to intercept, transform, or shape error responses before the handler runs.

This feature request proposes NestJS-compatible Pipes as a first-class PyNest primitive.


Motivation

Pipes serve two purposes:

  1. Transformation — coerce input to the expected type (e.g., "123"123)
  2. Validation — reject invalid data before it reaches the handler

Without explicit pipes:

  • Type coercion errors produce raw FastAPI/Pydantic 422 responses that don't match your API's error shape
  • There's no reusable way to add custom validation logic across routes
  • Input sanitization is scattered inside handler bodies

Proposed API

PipeTransform — base interface

```python
from abc import ABC, abstractmethod
from nest.common.pipes import PipeTransform, ArgumentMetadata

class PipeTransform(ABC):
@AbstractMethod
def transform(self, value: any, metadata: ArgumentMetadata) -> any:
...

class ArgumentMetadata:
metatype: type # e.g. int, str, CreateUserDto
type: str # 'body' | 'query' | 'param' | 'custom'
data: str | None # e.g. 'user_id' for @param('user_id')
```


Built-in Pipes

ValidationPipe — Pydantic model validation with shaped errors

```python
from nest.common.pipes import ValidationPipe

Global scope:

app.use_global_pipes(ValidationPipe(whitelist=True, transform=True))

Controller scope:

@controller('/users')
@UsePipes(ValidationPipe)
class UserController:
...

Route scope:

@post('/')
@UsePipes(ValidationPipe(whitelist=True))
def create_user(self, body: CreateUserDto):
...
```

ValidationPipe options:

Option Default Description
whitelist False Strip properties not in the DTO
forbid_non_whitelisted False Raise 400 if unknown properties sent
transform False Auto-coerce primitives to declared types
disable_error_messages False Hide validation detail
error_http_status_code 422 Status code on validation failure

ParseIntPipe

```python
@get('/:id')
@UsePipes(ParseIntPipe)
def get_user(self, id: str):
# id is now guaranteed int, raises 400 if not parseable
...

Or per-param (requires Feature #4 — Custom Param Decorators):

def get_user(self, @param('id', ParseIntPipe) id: int):
...
```

ParseFloatPipe, ParseBoolPipe, ParseUUIDPipe, ParseEnumPipe

```python
@get('/flag')
def flag(self, @query('active', ParseBoolPipe) active: bool):
...

@get('/:uuid')
def by_uuid(self, @param('uuid', ParseUUIDPipe) uid: UUID):
...
```

ParseArrayPipe

```python
@get('/bulk')
def bulk(self, @query('ids', ParseArrayPipe(items=ParseIntPipe, separator=',')) ids: list[int]):
# ?ids=1,2,3 → [1, 2, 3]
...
```

DefaultValuePipe

```python
@get('/')
def list(self,
@query('page', DefaultValuePipe(1), ParseIntPipe) page: int,
@query('limit', DefaultValuePipe(20), ParseIntPipe) limit: int,
):
...
```


Custom Pipes

```python
from nest.common.pipes import PipeTransform, ArgumentMetadata
from nest.common.exceptions import BadRequestException

class PositiveIntPipe(PipeTransform):
def transform(self, value: any, metadata: ArgumentMetadata) -> int:
val = int(value)
if val <= 0:
raise BadRequestException(f"{metadata.data} must be a positive integer")
return val

Usage:

@get('/:page')
def paginate(self, @param('page', PositiveIntPipe) page: int):
...
```


@UsePipes decorator

```python
from nest.common.decorators import UsePipes

Route level

@post('/')
@UsePipes(new ValidationPipe(whitelist=True))
def create(self, body: CreateUserDto): ...

Controller level — applies to all routes

@controller('/users')
@UsePipes(ValidationPipe)
class UserController: ...
```


app.use_global_pipes()

```python
app = PyNestFactory.create(AppModule)
app.use_global_pipes(ValidationPipe(transform=True, whitelist=True))
```


Pipe Execution Order

  1. Global pipes (registered via use_global_pipes)
  2. Controller-level @UsePipes
  3. Route-level @UsePipes
  4. Param-level pipes (e.g., @Param('id', ParseIntPipe))

Each pipe receives the output of the previous pipe.


Acceptance Criteria

  • PipeTransform abstract base class with transform(value, metadata) in nest/common/pipes.py
  • ArgumentMetadata dataclass with metatype, type, data fields
  • @UsePipes(*pipes) decorator for controller and route scope
  • app.use_global_pipes(*pipes) API
  • ValidationPipe with whitelist, forbid_non_whitelisted, transform, error_http_status_code options
  • ParseIntPipe, ParseFloatPipe, ParseBoolPipe, ParseUUIDPipe, ParseEnumPipe built-in pipes
  • ParseArrayPipe(items=pipe, separator=",") built-in pipe
  • DefaultValuePipe(default) built-in pipe
  • Pipe chaining support (multiple pipes per param)
  • Pipe execution order matches priority above
  • Unit tests for all built-in pipes and custom pipe patterns
  • Documentation page

Dependencies


Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions