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 mechanism to wrap route handler execution with cross-cutting logic. There is no way to transparently add logging, timing, response caching, response mapping, or request transformation without modifying handler code.
This feature request proposes NestJS-compatible Interceptors — a composable layer that wraps handler execution before AND after it runs.
Motivation
Interceptors are the most powerful tool in the NestJS pipeline because they can:
Completely override the handler (serve from cache, short-circuit)
Without interceptors, developers must either:
Add logging/timing boilerplate inside every handler
Write FastAPI middleware that operates on raw ASGI scope (no DI access)
Duplicate response transformation logic across services
Proposed API
NestInterceptor — base interface
```python
from abc import ABC, abstractmethod
from nest.common.interceptors import NestInterceptor, ExecutionContext, CallHandler
from typing import Observable # we use AsyncGenerator as Python's equivalent
Overview
PyNest has no mechanism to wrap route handler execution with cross-cutting logic. There is no way to transparently add logging, timing, response caching, response mapping, or request transformation without modifying handler code.
This feature request proposes NestJS-compatible Interceptors — a composable layer that wraps handler execution before AND after it runs.
Motivation
Interceptors are the most powerful tool in the NestJS pipeline because they can:
Without interceptors, developers must either:
Proposed API
NestInterceptor— base interface```python
from abc import ABC, abstractmethod
from nest.common.interceptors import NestInterceptor, ExecutionContext, CallHandler
from typing import Observable # we use AsyncGenerator as Python's equivalent
class NestInterceptor(ABC):
@AbstractMethod
async def intercept(self, context: ExecutionContext, next: CallHandler) -> any:
...
class CallHandler:
async def handle(self) -> any:
"""Call the next handler in the chain (ultimately the route handler)."""
...
```
@UseInterceptorsdecorator```python
from nest.common.decorators import UseInterceptors
@controller('/users')
@UseInterceptors(LoggingInterceptor) # controller-level
class UserController:
```
Built-in Interceptors
LoggingInterceptor```python
from nest.common.interceptors import NestInterceptor, ExecutionContext, CallHandler
import time, logging
class LoggingInterceptor(NestInterceptor):
async def intercept(self, context: ExecutionContext, next: CallHandler) -> any:
req = context.switch_to_http().get_request()
logger.info(f"→ {req.method} {req.url}")
start = time.perf_counter()
```
TimeoutInterceptor```python
import asyncio
class TimeoutInterceptor(NestInterceptor):
def init(self, timeout_ms: int = 5000):
self.timeout = timeout_ms / 1000
```
CacheInterceptor```python
class CacheInterceptor(NestInterceptor):
def init(self, cache_service: CacheService):
self.cache = cache_service
```
TransformInterceptor— wrap every response in{data: ...}```python
class TransformInterceptor(NestInterceptor):
async def intercept(self, context: ExecutionContext, next: CallHandler) -> any:
result = await next.handle()
return {"data": result, "statusCode": 200}
```
ExcludeNullInterceptor— strip None values from responses```python
class ExcludeNullInterceptor(NestInterceptor):
async def intercept(self, context: ExecutionContext, next: CallHandler) -> any:
result = await next.handle()
return self._strip_nulls(result)
```
ExecutionContext— rich context object```python
class ExecutionContext:
def switch_to_http(self) -> HttpArgumentsHost: ...
def get_class(self) -> type: ... # the controller class
def get_handler(self) -> callable: ... # the route method
def get_type(self) -> str: ... # 'http' | 'ws'
```
app.use_global_interceptors()```python
app = PyNestFactory.create(AppModule)
app.use_global_interceptors(LoggingInterceptor(), TransformInterceptor())
```
Interceptor Execution Order
For a route with both controller-level and route-level interceptors:
Each interceptor wraps the next, forming a true middleware onion.
DI Support in Interceptors
Interceptors registered with
@UseInterceptors(MyInterceptor)should be instantiated via the DI container, allowing them to declare dependencies:```python
@Injectable
class AuditInterceptor(NestInterceptor):
def init(self, audit_service: AuditService): # injected!
self.audit = audit_service
```
Acceptance Criteria
NestInterceptorabstract base class innest/common/interceptors.pyCallHandlerclass with asynchandle()methodExecutionContextwithswitch_to_http(),get_class(),get_handler(),get_type()@UseInterceptors(*interceptors)decorator for controller and route scopeapp.use_global_interceptors(*interceptors)APILoggingInterceptor,TimeoutInterceptor,TransformInterceptor,ExcludeNullInterceptorintercept()methods supportedDependencies
ExecutionContextshares infrastructure with hooksArgumentsHost/HttpArgumentsHostwith Exception FiltersRelated