NitroStack LogoNitroStack
/sdk
/typescript
/interceptors

Interceptors Guide

Overview

Interceptors bind additional logic before and after handler execution. Unlike middleware, interceptors focus on transforming requests and responses, adding metadata, or implementing cross-cutting patterns like caching and response wrapping.

Table of Contents

Creating Interceptors

Basic Interceptor

Interceptors implement the InterceptorInterface:

Typescript
import { Interceptor, InterceptorInterface, ExecutionContext } from '@nitrostack/core';

@Interceptor()
export class ResponseWrapperInterceptor implements InterceptorInterface {
  async intercept(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
    const startTime = Date.now();

    // Execute handler
    const result = await next();

    // Transform response
    return {
      success: true,
      data: result,
      metadata: {
        tool: context.toolName,
        requestId: context.metadata?.requestId || context.requestId,
        timestamp: new Date().toISOString(),
        duration: Date.now() - startTime
      }
    };
  }
}

Interceptor Interface

Typescript
interface InterceptorInterface {
  intercept(context: ExecutionContext, next: () => Promise<any>): Promise<any>;
}

Parameters:

  • context: Execution context with auth, logger, and metadata
  • next: Function to call the handler

Return: Transformed response

Using Interceptors

On Individual Methods

Typescript
import { ToolDecorator as Tool, UseInterceptors } from '@nitrostack/core';
import { ResponseWrapperInterceptor } from './interceptors/response-wrapper.interceptor.js';

export class ProductTools {
  @Tool({ name: 'get_product' })
  @UseInterceptors(ResponseWrapperInterceptor)
  async getProduct(input: { productId: string }, ctx: ExecutionContext) {
    return this.productService.findById(input.productId);
    // Returns: { success: true, data: {...product}, metadata: {...} }
  }
}

Multiple Interceptors

Typescript
@Tool({ name: 'get_user' })
@UseInterceptors(
  ResponseWrapperInterceptor,
  DataMaskingInterceptor,
  CacheInterceptor
)
async getUser(input: { userId: string }, ctx: ExecutionContext) {
  return this.userService.findById(input.userId);
}

Common Patterns

Response Transformation

Typescript
@Interceptor()
export class TransformInterceptor implements InterceptorInterface {
  async intercept(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
    const result = await next();
    return this.transformKeys(result);
  }

  private transformKeys(data: unknown): unknown {
    if (data === null || data === undefined) return data;

    if (Array.isArray(data)) {
      return data.map(item => this.transformKeys(item));
    }

    if (typeof data === 'object' && !(data instanceof Date)) {
      const transformed: Record<string, unknown> = {};
      for (const [key, value] of Object.entries(data)) {
        // Convert snake_case to camelCase
        const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
        transformed[camelKey] = this.transformKeys(value);
      }
      return transformed;
    }

    return data;
  }
}

Sensitive Data Masking

Typescript
@Interceptor()
export class DataMaskingInterceptor implements InterceptorInterface {
  private static readonly SENSITIVE_FIELDS = [
    'password',
    'ssn',
    'socialSecurityNumber',
    'creditCard',
    'cardNumber',
    'cvv',
    'apiKey',
    'secretKey',
    'accessToken'
  ];

  async intercept(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
    const result = await next();
    return this.maskSensitiveData(result);
  }

  private maskSensitiveData(data: unknown): unknown {
    if (data === null || data === undefined) return data;

    if (Array.isArray(data)) {
      return data.map(item => this.maskSensitiveData(item));
    }

    if (typeof data === 'object' && !(data instanceof Date)) {
      const masked: Record<string, unknown> = {};
      for (const [key, value] of Object.entries(data)) {
        if (this.isSensitiveField(key)) {
          masked[key] = this.maskValue(key, value);
        } else {
          masked[key] = this.maskSensitiveData(value);
        }
      }
      return masked;
    }

    return data;
  }

  private isSensitiveField(field: string): boolean {
    const lowerField = field.toLowerCase();
    return DataMaskingInterceptor.SENSITIVE_FIELDS.some(
      sensitive => lowerField.includes(sensitive.toLowerCase())
    );
  }

  private maskValue(field: string, value: unknown): string {
    if (!value) return '***';
    const str = String(value);

    if (field.toLowerCase().includes('ssn')) {
      return `***-**-${str.slice(-4)}`;
    }

    if (field.toLowerCase().includes('card') || field.toLowerCase().includes('credit')) {
      return `****-****-****-${str.slice(-4)}`;
    }

    if (field.toLowerCase().includes('key') || field.toLowerCase().includes('token')) {
      if (str.length > 8) {
        return `${str.slice(0, 4)}...${str.slice(-4)}`;
      }
    }

    return '********';
  }
}

Response Caching

Typescript
import { Injectable } from '@nitrostack/core';

@Injectable()
@Interceptor()
export class CacheInterceptor implements InterceptorInterface {
  private cache = new Map<string, { data: unknown; expiresAt: number }>();

  async intercept(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
    const cacheKey = this.generateCacheKey(context);

    // Check cache
    const cached = this.cache.get(cacheKey);
    if (cached && cached.expiresAt > Date.now()) {
      context.logger.info('Cache hit', { key: cacheKey });
      return cached.data;
    }

    // Execute handler
    const result = await next();

    // Store in cache (5 minute TTL)
    this.cache.set(cacheKey, {
      data: result,
      expiresAt: Date.now() + 5 * 60 * 1000
    });

    return result;
  }

  private generateCacheKey(context: ExecutionContext): string {
    const toolName = context.toolName || 'unknown';
    const input = JSON.stringify(context.metadata?.input || {});
    return `${toolName}:${input}`;
  }
}

Error Response Formatting

Typescript
@Interceptor()
export class ErrorFormatInterceptor implements InterceptorInterface {
  async intercept(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
    try {
      return await next();
    } catch (error) {
      return {
        success: false,
        error: {
          code: this.getErrorCode(error),
          message: error instanceof Error ? error.message : 'An error occurred',
          timestamp: new Date().toISOString(),
          requestId: context.metadata?.requestId
        }
      };
    }
  }

  private getErrorCode(error: unknown): string {
    if (error instanceof ValidationError) return 'VALIDATION_ERROR';
    if (error instanceof NotFoundError) return 'NOT_FOUND';
    if (error instanceof UnauthorizedError) return 'UNAUTHORIZED';
    return 'INTERNAL_ERROR';
  }
}

Pagination Wrapper

Typescript
@Interceptor()
export class PaginationInterceptor implements InterceptorInterface {
  async intercept(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
    const result = await next();

    // Check if result is paginated data
    if (Array.isArray(result) && context.metadata?.input) {
      const input = context.metadata.input as {
        page?: number;
        limit?: number;
        total?: number;
      };

      const page = input.page || 1;
      const limit = input.limit || 20;

      return {
        data: result,
        pagination: {
          page,
          limit,
          total: input.total || result.length,
          hasMore: result.length === limit
        }
      };
    }

    return result;
  }
}

Dependency Injection

Interceptors support dependency injection:

Typescript
import { Injectable, Interceptor, InterceptorInterface } from '@nitrostack/core';

@Injectable()
@Interceptor()
export class AuditInterceptor implements InterceptorInterface {
  constructor(
    private auditService: AuditService,
    private configService: ConfigService
  ) {}

  async intercept(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
    const startTime = Date.now();
    const shouldAudit = this.configService.get('ENABLE_AUDIT', true);

    const result = await next();

    if (shouldAudit) {
      await this.auditService.record({
        action: context.toolName,
        userId: context.auth?.subject,
        duration: Date.now() - startTime,
        timestamp: new Date()
      });
    }

    return result;
  }
}

Best Practices

1. Do Not Mutate Original Data

Return new objects instead of modifying the original:

Typescript
// Correct: Return new object
async intercept(context, next) {
  const result = await next();
  return {
    ...result,
    transformed: true
  };
}

// Incorrect: Mutating original
async intercept(context, next) {
  const result = await next();
  result.transformed = true;  // Mutation
  return result;
}

2. Handle Errors Gracefully

Decide whether to catch errors or let them propagate:

Typescript
// Transform errors
async intercept(context, next) {
  try {
    return await next();
  } catch (error) {
    return { error: true, message: error.message };
  }
}

// Or propagate with logging
async intercept(context, next) {
  try {
    return await next();
  } catch (error) {
    context.logger.error('Interceptor caught error', { error });
    throw error;  // Re-throw for upstream handling
  }
}

3. Keep Interceptors Focused

Each interceptor should have a single transformation purpose:

Typescript
// Correct: Focused interceptors
@Interceptor()
export class ResponseWrapperInterceptor { /* Wraps responses */ }

@Interceptor()
export class DataMaskingInterceptor { /* Masks sensitive data */ }

// Incorrect: Combined responsibilities
@Interceptor()
export class DoEverythingInterceptor {
  async intercept(context, next) {
    // Wrapping + masking + caching + logging
  }
}

4. Document Transformations

Clearly document what the interceptor modifies:

Typescript
/**
 * Response Wrapper Interceptor
 *
 * Transforms handler output into standardized response format:
 * {
 *   success: boolean,
 *   data: T,
 *   metadata: { tool, requestId, timestamp, duration }
 * }
 */
@Interceptor()
export class ResponseWrapperInterceptor implements InterceptorInterface {
  // Implementation
}