You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
Transformation — coerce input to the expected type (e.g., "123" → 123)
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
```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
Unit tests for all built-in pipes and custom pipe patterns
Documentation page
Dependencies
Feature update readme #1 (Exception Filters) — pipes should throw BadRequestException / UnprocessableEntityException which filters can then catch and shape
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:
"123"→123)Without explicit pipes:
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):
...
```
ValidationPipeoptions:whitelistFalseforbid_non_whitelistedFalsetransformFalsedisable_error_messagesFalseerror_http_status_code422ParseIntPipe```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):
...
```
@UsePipesdecorator```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
use_global_pipes)@UsePipes@UsePipes@Param('id', ParseIntPipe))Each pipe receives the output of the previous pipe.
Acceptance Criteria
PipeTransformabstract base class withtransform(value, metadata)innest/common/pipes.pyArgumentMetadatadataclass withmetatype,type,datafields@UsePipes(*pipes)decorator for controller and route scopeapp.use_global_pipes(*pipes)APIValidationPipewithwhitelist,forbid_non_whitelisted,transform,error_http_status_codeoptionsParseIntPipe,ParseFloatPipe,ParseBoolPipe,ParseUUIDPipe,ParseEnumPipebuilt-in pipesParseArrayPipe(items=pipe, separator=",")built-in pipeDefaultValuePipe(default)built-in pipeDependencies
BadRequestException/UnprocessableEntityExceptionwhich filters can then catch and shapeRelated