Repository navigation
Feature: Pipes & ValidationPipe (@UsePipes, PipeTransform, ParseIntPipe, etc.) #116
Description
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:
- 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
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
- Global pipes (registered via
use_global_pipes) - Controller-level
@UsePipes - Route-level
@UsePipes - Param-level pipes (e.g.,
@Param('id', ParseIntPipe))
Each pipe receives the output of the previous pipe.
Acceptance Criteria
-
PipeTransformabstract base class withtransform(value, metadata)innest/common/pipes.py -
ArgumentMetadatadataclass withmetatype,type,datafields -
@UsePipes(*pipes)decorator for controller and route scope -
app.use_global_pipes(*pipes)API -
ValidationPipewithwhitelist,forbid_non_whitelisted,transform,error_http_status_codeoptions -
ParseIntPipe,ParseFloatPipe,ParseBoolPipe,ParseUUIDPipe,ParseEnumPipebuilt-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
- Feature update readme #1 (Exception Filters) — pipes should throw
BadRequestException/UnprocessableEntityExceptionwhich filters can then catch and shape - Feature Dependency Injection Initialize depend service for each request #4 (Custom Param Decorators) — param-level pipe argument requires param decorator support
Related
- NestJS docs: https://docs.nestjs.com/pipes
Activity