Interceptors Guide
Overview
Interceptors bind extra logic before or after tool execution. They can transform requests, modify responses, or add metadata.
Creating an Interceptor
import { Interceptor, InterceptorInterface, ExecutionContext } from 'nitrostack';
@Interceptor()
export class TransformInterceptor implements InterceptorInterface {
async intercept(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
// Execute tool
const result = await next();
// Transform response
return {
success: true,
data: result,
metadata: {
timestamp: new Date().toISOString(),
tool: context.toolName
}
};
}
}
Using Interceptors
On a Tool
@Tool({ name: 'get_product' })
@UseInterceptors(TransformInterceptor)
async getProduct(input: any, ctx: ExecutionContext) {
return { id: 1, name: 'Product' };
// Returns: { success: true, data: { id: 1, name: 'Product' }, metadata: {...} }
}
Multiple Interceptors
@Tool({ name: 'create_user' })
@UseInterceptors(TransformInterceptor, LoggingInterceptor, CacheInterceptor)
async createUser(input: any, ctx: ExecutionContext) {
// All interceptors run in order
}
Interceptor Examples
Response Wrapper
@Interceptor()
export class ResponseWrapperInterceptor implements InterceptorInterface {
async intercept(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
const start = Date.now();
try {
const result = await next();
return {
success: true,
data: result,
meta: {
duration: Date.now() - start,
timestamp: new Date().toISOString()
}
};
} catch (error) {
return {
success: false,
error: error.message,
meta: {
duration: Date.now() - start,
timestamp: new Date().toISOString()
}
};
}
}
}
Data Transformation
@Interceptor()
export class SnakeToCamelInterceptor implements InterceptorInterface {
async intercept(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
const result = await next();
return this.transformKeys(result);
}
private transformKeys(obj: any): any {
if (Array.isArray(obj)) {
return obj.map(item => this.transformKeys(item));
}
if (obj && typeof obj === 'object') {
return Object.keys(obj).reduce((acc, key) => {
const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
acc[camelKey] = this.transformKeys(obj[key]);
return acc;
}, {} as any);
}
return obj;
}
}
Sensitive Data Masking
@Interceptor()
export class MaskingInterceptor implements InterceptorInterface {
private sensitiveFields = ['password', 'ssn', 'creditCard'];
async intercept(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
const result = await next();
return this.maskSensitiveData(result);
}
private maskSensitiveData(obj: any): any {
if (Array.isArray(obj)) {
return obj.map(item => this.maskSensitiveData(item));
}
if (obj && typeof obj === 'object') {
const masked: any = {};
for (const [key, value] of Object.entries(obj)) {
if (this.sensitiveFields.includes(key)) {
masked[key] = '***MASKED***';
} else {
masked[key] = this.maskSensitiveData(value);
}
}
return masked;
}
return obj;
}
}
Best Practices
- Don't mutate original data - Return new objects
- Handle errors gracefully - Catch and transform
- Keep interceptors simple - Single responsibility
- Use DI for services - Injectable interceptors
- Document transformations - Clear JSDoc