Interceptors
Interceptors are a powerful feature that allows you to bind extra logic before and after method execution. They can transform the result returned from a function, extend the basic function behavior, or completely override the handler function.
Overview
Interceptors have a set of useful capabilities inspired by the Aspect-Oriented Programming (AOP) technique:
- Transform the result before returning
- Bind extra logic before/after method execution
- Measure execution time for performance monitoring
- Catch exceptions and transform them
- Cache responses for improved performance
- Logging and auditing of requests
Creating an Interceptor
An interceptor implements the Interceptor interface with an intercept() method:
import { Injectable, Interceptor, ExecutionContext, CallHandler } from '@riktajs/core';
@Injectable()
export class LoggingInterceptor implements Interceptor {
async intercept(context: ExecutionContext, next: CallHandler): Promise<unknown> {
console.log('Before handler execution...');
const start = Date.now();
const result = await next.handle();
const duration = Date.now() - start;
console.log(`After handler execution... (${duration}ms)`);
return result;
}
}
Using Interceptors
Controller-level
Apply to all routes in a controller:
import { Controller, Get, UseInterceptors } from '@riktajs/core';
import { LoggingInterceptor } from './interceptors/logging.interceptor';
@Controller('/users')
@UseInterceptors(LoggingInterceptor)
export class UserController {
@Get('/')
findAll() {
return [{ id: 1, name: 'John' }];
}
@Get('/:id')
findOne() {
return { id: 1, name: 'John' };
}
}
Route-level
Apply to a specific route:
@Controller('/users')
export class UserController {
@Get('/')
@UseInterceptors(LoggingInterceptor)
findAll() {
return [{ id: 1, name: 'John' }];
}
@Get('/:id')
findOne() {
// No interceptor here
return { id: 1, name: 'John' };
}
}
Multiple Interceptors
Apply multiple interceptors (executed in order):
@Controller('/users')
@UseInterceptors(LoggingInterceptor, CacheInterceptor, TransformInterceptor)
export class UserController {
// ...
}
Execution Order (Onion Model)
When multiple interceptors are applied, they wrap around each other like an onion:
Request → [Interceptor1 before] → [Interceptor2 before] → Handler → [Interceptor2 after] → [Interceptor1 after] → Response
Example with timing:
@Injectable()
class FirstInterceptor implements Interceptor {
async intercept(context: ExecutionContext, next: CallHandler): Promise<unknown> {
console.log('First - Before');
const result = await next.handle();
console.log('First - After');
return result;
}
}
@Injectable()
class SecondInterceptor implements Interceptor {
async intercept(context: ExecutionContext, next: CallHandler): Promise<unknown> {
console.log('Second - Before');
const result = await next.handle();
console.log('Second - After');
return result;
}
}
// Output order:
// First - Before
// Second - Before
// (handler executes)
// Second - After
// First - After
Common Use Cases
Response Transformation
Transform the response structure:
@Injectable()
export class TransformInterceptor implements Interceptor {
async intercept(context: ExecutionContext, next: CallHandler): Promise<unknown> {
const result = await next.handle();
return {
data: result,
timestamp: new Date().toISOString(),
success: true,
};
}
}
Response Caching
Cache responses for performance:
@Injectable()
export class CacheInterceptor implements Interceptor {
private cache = new Map<string, { data: unknown; expires: number }>();
async intercept(context: ExecutionContext, next: CallHandler): Promise<unknown> {
const request = context.switchToHttp().getRequest();
const cacheKey = `${request.method}:${request.url}`;
const cached = this.cache.get(cacheKey);
if (cached && cached.expires > Date.now()) {
return cached.data;
}
const result = await next.handle();
this.cache.set(cacheKey, {
data: result,
expires: Date.now() + 60000, // 1 minute TTL
});
return result;
}
}
Error Handling
Catch and transform errors:
@Injectable()
export class ErrorInterceptor implements Interceptor {
async intercept(context: ExecutionContext, next: CallHandler): Promise<unknown> {
try {
return await next.handle();
} catch (error) {
const request = context.switchToHttp().getRequest();
console.error(`Error in ${request.method} ${request.url}:`, error);
// Return a friendly error response
return {
error: true,
message: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString(),
};
}
}
}
Request Timing
Track request duration:
@Injectable()
export class TimingInterceptor implements Interceptor {
@Autowired(MetricsService)
private metrics!: MetricsService;
async intercept(context: ExecutionContext, next: CallHandler): Promise<unknown> {
const request = context.switchToHttp().getRequest();
const start = Date.now();
try {
return await next.handle();
} finally {
const duration = Date.now() - start;
this.metrics.recordRequestDuration(request.url, duration);
}
}
}
Accessing ExecutionContext
The ExecutionContext provides access to request details:
@Injectable()
export class ContextAwareInterceptor implements Interceptor {
async intercept(context: ExecutionContext, next: CallHandler): Promise<unknown> {
// Access HTTP context
const httpContext = context.switchToHttp();
const request = httpContext.getRequest();
const response = httpContext.getResponse();
// Get metadata about the handler
const controllerClass = context.getClass();
const handlerName = context.getHandler();
console.log(`Handling ${controllerClass.name}.${String(handlerName)}`);
return next.handle();
}
}
Dependency Injection
Interceptors support full dependency injection:
@Injectable()
export class AuditInterceptor implements Interceptor {
@Autowired(AuditService)
private auditService!: AuditService;
@Autowired(UserService)
private userService!: UserService;
async intercept(context: ExecutionContext, next: CallHandler): Promise<unknown> {
const request = context.switchToHttp().getRequest();
const user = await this.userService.getCurrentUser(request);
await this.auditService.logAccess({
userId: user?.id,
action: `${request.method} ${request.url}`,
timestamp: new Date(),
});
return next.handle();
}
}
Interceptor Caching
Interceptor instances are cached and reused across routes, similar to guards and middleware. This ensures:
- Consistent state for singleton interceptors
- Performance optimization by avoiding repeated instantiation
- Memory efficiency with shared instances
// Both routes share the same LoggingInterceptor instance
@Controller('/users')
class UserController {
@Get('/one')
@UseInterceptors(LoggingInterceptor)
one() { ... }
@Get('/two')
@UseInterceptors(LoggingInterceptor)
two() { ... }
}
API Reference
Interceptor Interface
interface Interceptor {
intercept(context: ExecutionContext, next: CallHandler): Promise<unknown>;
}
CallHandler Interface
interface CallHandler {
handle(): Promise<unknown>;
}
ExecutionContext
interface ExecutionContext {
switchToHttp(): HttpArgumentsHost;
getRequest<T>(): T; // deprecated
getReply<T>(): T; // deprecated
getClass(): Constructor;
getHandler(): string | symbol;
getMetadata<T>(key: string | symbol): T | undefined;
}
interface HttpArgumentsHost {
getRequest<T>(): T;
getResponse<T>(): T;
}