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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions