Overview
PyNest route handlers today rely entirely on FastAPI's implicit parameter binding — function signature type hints are parsed by FastAPI directly. This means there is no PyNest-native way to:
- Explicitly declare where a parameter comes from (
@Body, @Param, @Query)
- Build reusable domain-specific decorators like
@CurrentUser() or @TenantId()
- Transform or validate individual parameters before they reach the handler
This feature request proposes a full suite of built-in param decorators and a public createParamDecorator factory API.
Motivation
```python
Today — implicit and ambiguous:
@post('/users')
def create_user(self, user_data: CreateUserDto, token: str = Header(None)):
...
With this feature — explicit and composable:
@post('/users')
def create_user(self, @Body() user_data: CreateUserDto, @currentuser() user: User):
...
```
Explicit param decorators also enable:
Built-in Param Decorators
@Body(key?: str)
```python
@post('/users')
def create_user(self, @Body() body: CreateUserDto):
# body is the full request body, validated against CreateUserDto
...
@post('/upload')
def upload(self, @Body('file') file: UploadFile):
...
```
@Param(name?: str)
```python
@get('/:user_id/posts/:post_id')
def get_post(self, @Param('user_id') uid: int, @Param('post_id') pid: int):
...
@get('/:user_id')
def get_user(self, @Param() params: dict):
# params = {"user_id": "123"}
...
```
@Query(name?: str)
```python
@get('/search')
def search(self, @query('q') query: str, @query('limit') limit: int = 20):
...
```
@Headers(name?: str)
```python
@get('/me')
def get_me(self, @headers('authorization') auth: str):
...
@get('/debug')
def debug(self, @headers() all_headers: dict):
...
```
@Req() and @Res()
```python
@get('/raw')
def raw(self, @Req() request: Request, @res() response: Response):
...
```
@Ip() and @HostParam()
```python
@post('/audit')
def audit(self, @ip() ip: str):
...
```
createParamDecorator — Custom Param Decorators
The real power: a public factory for building reusable param decorators with arbitrary extraction logic.
```python
from nest.common.decorators import createParamDecorator, ExecutionContext
Define once:
CurrentUser = createParamDecorator(
lambda data, ctx: ctx.switch_to_http().get_request().state.user
)
TenantId = createParamDecorator(
lambda data, ctx: ctx.switch_to_http().get_request().headers.get("x-tenant-id")
)
Pagination = createParamDecorator(
lambda data, ctx: {
"page": int(ctx.switch_to_http().get_request().query_params.get("page", 1)),
"limit": int(ctx.switch_to_http().get_request().query_params.get("limit", 20)),
}
)
Use anywhere:
@get('/posts')
def list_posts(self, @currentuser() user: User, @Pagination() pagination: dict):
return self.service.get_posts(user.id, **pagination)
@delete('/:id')
def delete_item(self, @Param('id') item_id: int, @TenantID() tenant: str):
...
```
data argument — parametrized decorators
```python
UserProperty = createParamDecorator(
lambda data, ctx: getattr(ctx.switch_to_http().get_request().state.user, data, None)
)
@get('/profile')
def profile(self, @userproperty('email') email: str):
...
```
Integration with Pipes (Feature #5)
Param decorators should accept an optional pipe argument for per-parameter transformation:
```python
@get('/:id')
def get(self, @Param('id', ParseIntPipe) id: int):
...
@post('/')
def create(self, @Body(ValidationPipe) body: CreateDto):
...
```
Implementation Approach
Since Python does not have native parameter-level decorators, this should be implemented via:
- A
ParamMetadata descriptor or a sentinel object returned by each decorator
- The controller decorator (
@Controller) or a route processing wrapper that inspects function signatures for these sentinels
- FastAPI
Depends() under the hood for actual extraction and injection
Acceptance Criteria
Related
Overview
PyNest route handlers today rely entirely on FastAPI's implicit parameter binding — function signature type hints are parsed by FastAPI directly. This means there is no PyNest-native way to:
@Body,@Param,@Query)@CurrentUser()or@TenantId()This feature request proposes a full suite of built-in param decorators and a public
createParamDecoratorfactory API.Motivation
```python
Today — implicit and ambiguous:
@post('/users')
def create_user(self, user_data: CreateUserDto, token: str = Header(None)):
...
With this feature — explicit and composable:
@post('/users')
def create_user(self, @Body() user_data: CreateUserDto, @currentuser() user: User):
...
```
Explicit param decorators also enable:
Built-in Param Decorators
@Body(key?: str)```python
@post('/users')
def create_user(self, @Body() body: CreateUserDto):
# body is the full request body, validated against CreateUserDto
...
@post('/upload')
def upload(self, @Body('file') file: UploadFile):
...
```
@Param(name?: str)```python
@get('/:user_id/posts/:post_id')
def get_post(self, @Param('user_id') uid: int, @Param('post_id') pid: int):
...
@get('/:user_id')
def get_user(self, @Param() params: dict):
# params = {"user_id": "123"}
...
```
@Query(name?: str)```python
@get('/search')
def search(self, @query('q') query: str, @query('limit') limit: int = 20):
...
```
@Headers(name?: str)```python
@get('/me')
def get_me(self, @headers('authorization') auth: str):
...
@get('/debug')
def debug(self, @headers() all_headers: dict):
...
```
@Req()and@Res()```python
@get('/raw')
def raw(self, @Req() request: Request, @res() response: Response):
...
```
@Ip()and@HostParam()```python
@post('/audit')
def audit(self, @ip() ip: str):
...
```
createParamDecorator— Custom Param DecoratorsThe real power: a public factory for building reusable param decorators with arbitrary extraction logic.
```python
from nest.common.decorators import createParamDecorator, ExecutionContext
Define once:
CurrentUser = createParamDecorator(
lambda data, ctx: ctx.switch_to_http().get_request().state.user
)
TenantId = createParamDecorator(
lambda data, ctx: ctx.switch_to_http().get_request().headers.get("x-tenant-id")
)
Pagination = createParamDecorator(
lambda data, ctx: {
"page": int(ctx.switch_to_http().get_request().query_params.get("page", 1)),
"limit": int(ctx.switch_to_http().get_request().query_params.get("limit", 20)),
}
)
Use anywhere:
@get('/posts')
def list_posts(self, @currentuser() user: User, @Pagination() pagination: dict):
return self.service.get_posts(user.id, **pagination)
@delete('/:id')
def delete_item(self, @Param('id') item_id: int, @TenantID() tenant: str):
...
```
dataargument — parametrized decorators```python
UserProperty = createParamDecorator(
lambda data, ctx: getattr(ctx.switch_to_http().get_request().state.user, data, None)
)
@get('/profile')
def profile(self, @userproperty('email') email: str):
...
```
Integration with Pipes (Feature #5)
Param decorators should accept an optional pipe argument for per-parameter transformation:
```python
@get('/:id')
def get(self, @Param('id', ParseIntPipe) id: int):
...
@post('/')
def create(self, @Body(ValidationPipe) body: CreateDto):
...
```
Implementation Approach
Since Python does not have native parameter-level decorators, this should be implemented via:
ParamMetadatadescriptor or a sentinel object returned by each decorator@Controller) or a route processing wrapper that inspects function signatures for these sentinelsDepends()under the hood for actual extraction and injectionAcceptance Criteria
@Body(key?),@Param(name?),@Query(name?),@Headers(name?)built-in decorators@Req(),@Res(),@Ip(),@HostParam()built-in decoratorscreateParamDecorator(factory)public APIdataargument for parameterized use@Param('id', ParseIntPipe))ExecutionContextwithswitch_to_http()available inside factory functionscreateParamDecoratorexamplesRelated