Middleware Guide
Overview
Middleware functions execute before and after tool execution. They're perfect for cross-cutting concerns like logging, timing, and request processing.
Creating Middleware
Basic Middleware
import { Middleware, MiddlewareInterface, ExecutionContext } from 'nitrostack';
@Middleware()
export class LoggingMiddleware implements MiddlewareInterface {
  async use(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
    // Before tool execution
    context.logger.info(`Executing: ${context.toolName}`);
    const start = Date.now();
    
    try {
      // Call next middleware or tool
      const result = await next();
      
      // After successful execution
      const duration = Date.now() - start;
      context.logger.info(`Completed ${context.toolName} in ${duration}ms`);
      
      return result;
    } catch (error) {
      // After failed execution
      const duration = Date.now() - start;
      context.logger.error(`Failed ${context.toolName} after ${duration}ms:`, error);
      throw error;
    }
  }
}
Using Middleware
On a Single Tool
import { UseMiddleware } from 'nitrostack';
export class ProductTools {
  @Tool({ name: 'get_product' })
  @UseMiddleware(LoggingMiddleware)  // ← Apply middleware
  async getProduct(input: any, ctx: ExecutionContext) {
    return await this.productService.findById(input.product_id);
  }
}
On Multiple Tools
@Tool({ name: 'create_product' })
@UseMiddleware(LoggingMiddleware, ValidationMiddleware, TimingMiddleware)
async createProduct(input: any, ctx: ExecutionContext) {
  // All 3 middleware run in order
}
On All Tools in a Class
@UseMiddleware(LoggingMiddleware)  // ← Applies to all tools
export class ProductTools {
  @Tool({ name: 'get_product' })
  async getProduct(input: any, ctx: ExecutionContext) {}
  
  @Tool({ name: 'create_product' })
  async createProduct(input: any, ctx: ExecutionContext) {}
}
Middleware Examples
Request Logging
@Middleware()
export class RequestLoggingMiddleware implements MiddlewareInterface {
  async use(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
    const requestId = Math.random().toString(36).substring(7);
    
    context.logger.info(`[${requestId}] Request: ${context.toolName}`, {
      toolName: context.toolName,
      auth: context.auth?.subject,
      timestamp: new Date().toISOString()
    });
    
    try {
      const result = await next();
      
      context.logger.info(`[${requestId}] Response: Success`);
      return result;
    } catch (error) {
      context.logger.error(`[${requestId}] Response: Error`, error);
      throw error;
    }
  }
}
Performance Timing
@Middleware()
export class TimingMiddleware implements MiddlewareInterface {
  async use(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
    const start = Date.now();
    
    try {
      const result = await next();
      const duration = Date.now() - start;
      
      // Log slow requests
      if (duration > 1000) {
        context.logger.warn(`Slow request: ${context.toolName} took ${duration}ms`);
      }
      
      return result;
    } finally {
      const duration = Date.now() - start;
      context.metadata.duration = duration;
    }
  }
}
Request ID Injection
@Middleware()
export class RequestIdMiddleware implements MiddlewareInterface {
  async use(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
    // Generate unique request ID
    const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(7)}`;
    
    // Add to context
    context.metadata.requestId = requestId;
    
    // Add to all logs
    const originalLog = context.logger.info.bind(context.logger);
    context.logger.info = (...args: any[]) => {
      originalLog(`[${requestId}]`, ...args);
    };
    
    return await next();
  }
}
Error Handling
@Middleware()
export class ErrorHandlingMiddleware implements MiddlewareInterface {
  async use(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
    try {
      return await next();
    } catch (error) {
      if (error instanceof ValidationError) {
        context.logger.warn('Validation failed:', error.message);
        throw {
          code: 'VALIDATION_ERROR',
          message: error.message,
          details: error.details
        };
      }
      
      if (error instanceof DatabaseError) {
        context.logger.error('Database error:', error);
        throw {
          code: 'DATABASE_ERROR',
          message: 'An internal error occurred'
        };
      }
      
      // Unknown error
      context.logger.error('Unexpected error:', error);
      throw {
        code: 'INTERNAL_ERROR',
        message: 'An unexpected error occurred'
      };
    }
  }
}
Cache Check
@Injectable()
@Middleware()
export class CacheMiddleware implements MiddlewareInterface {
  constructor(private cacheService: CacheService) {}
  
  async use(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
    const cacheKey = `${context.toolName}:${JSON.stringify(context.metadata.input)}`;
    
    // Check cache
    const cached = await this.cacheService.get(cacheKey);
    if (cached) {
      context.logger.info('Cache hit:', cacheKey);
      return cached;
    }
    
    // Execute tool
    const result = await next();
    
    // Store in cache
    await this.cacheService.set(cacheKey, result, 300);  // 5 minutes
    
    return result;
  }
}
Rate Limiting
@Injectable()
@Middleware()
export class RateLimitMiddleware implements MiddlewareInterface {
  private requests = new Map<string, number[]>();
  
  constructor(
    private maxRequests = 10,
    private windowMs = 60000  // 1 minute
  ) {}
  
  async use(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
    const key = context.auth?.subject || 'anonymous';
    const now = Date.now();
    
    // Get request timestamps for this user
    const userRequests = this.requests.get(key) || [];
    
    // Remove old timestamps
    const validRequests = userRequests.filter(
      timestamp => now - timestamp < this.windowMs
    );
    
    // Check limit
    if (validRequests.length >= this.maxRequests) {
      throw new Error(`Rate limit exceeded. Max ${this.maxRequests} requests per minute.`);
    }
    
    // Add current request
    validRequests.push(now);
    this.requests.set(key, validRequests);
    
    return await next();
  }
}
Input Sanitization
@Middleware()
export class SanitizationMiddleware implements MiddlewareInterface {
  async use(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
    const input = context.metadata.input;
    
    if (input && typeof input === 'object') {
      // Remove potentially dangerous characters
      const sanitized = this.sanitizeObject(input);
      context.metadata.input = sanitized;
    }
    
    return await next();
  }
  
  private sanitizeObject(obj: any): any {
    if (typeof obj === 'string') {
      return obj
        .replace(/<script[^>]*>.*?<\/script>/gi, '')
        .replace(/<[^>]+>/g, '')
        .trim();
    }
    
    if (Array.isArray(obj)) {
      return obj.map(item => this.sanitizeObject(item));
    }
    
    if (obj && typeof obj === 'object') {
      const sanitized: any = {};
      for (const [key, value] of Object.entries(obj)) {
        sanitized[key] = this.sanitizeObject(value);
      }
      return sanitized;
    }
    
    return obj;
  }
}
Middleware Execution Order
Order Matters
@Tool({ name: 'example' })
@UseMiddleware(
  LoggingMiddleware,      // 1. Runs first
  AuthMiddleware,         // 2. Then auth
  ValidationMiddleware    // 3. Then validation
)
async example(input: any, ctx: ExecutionContext) {
  // 4. Tool executes
  // 5. ValidationMiddleware "after"
  // 6. AuthMiddleware "after"
  // 7. LoggingMiddleware "after"
}
Execution Flow
Request
   ↓
LoggingMiddleware (before)
   ↓
AuthMiddleware (before)
   ↓
ValidationMiddleware (before)
   ↓
Tool Execution
   ↓
ValidationMiddleware (after)
   ↓
AuthMiddleware (after)
   ↓
LoggingMiddleware (after)
   ↓
Response
Dependency Injection
Injectable Middleware
@Injectable()
@Middleware()
export class DatabaseMiddleware implements MiddlewareInterface {
  constructor(
    private db: DatabaseService,
    private logger: LoggerService
  ) {}
  
  async use(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
    // Use injected services
    const isHealthy = await this.db.healthCheck();
    
    if (!isHealthy) {
      this.logger.error('Database unhealthy');
      throw new Error('Service unavailable');
    }
    
    return await next();
  }
}
Conditional Middleware
Apply Based on Conditions
@Middleware()
export class ConditionalMiddleware implements MiddlewareInterface {
  async use(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
    // Only log in development
    if (process.env.NODE_ENV === 'development') {
      context.logger.info('Dev mode:', context.toolName);
    }
    
    return await next();
  }
}
Skip Middleware for Certain Tools
@Middleware()
export class OptionalMiddleware implements MiddlewareInterface {
  private skipTools = ['health_check', 'ping'];
  
  async use(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
    if (this.skipTools.includes(context.toolName || '')) {
      return await next();  // Skip middleware logic
    }
    
    // Normal middleware logic
    context.logger.info('Processing:', context.toolName);
    return await next();
  }
}
Best Practices
1. Keep Middleware Focused
// ✅ Good - Single responsibility
@Middleware()
export class LoggingMiddleware { }
@Middleware()
export class TimingMiddleware { }
// ❌ Avoid - Too much in one
@Middleware()
export class EverythingMiddleware { }
2. Always Call next()
// ✅ Good
async use(context: ExecutionContext, next: () => Promise<any>) {
  // Before logic
  const result = await next();  // ← Always call
  // After logic
  return result;
}
// ❌ Avoid - Breaks chain
async use(context: ExecutionContext, next: () => Promise<any>) {
  return { custom: 'response' };  // Never calls next()
}
3. Handle Errors Properly
// ✅ Good
async use(context: ExecutionContext, next: () => Promise<any>) {
  try {
    return await next();
  } catch (error) {
    // Log and re-throw
    context.logger.error('Error:', error);
    throw error;
  }
}
// ❌ Avoid - Swallow errors
async use(context: ExecutionContext, next: () => Promise<any>) {
  try {
    return await next();
  } catch (error) {
    return null;  // Silent failure!
  }
}
4. Use Dependency Injection
// ✅ Good
@Injectable()
@Middleware()
export class MyMiddleware {
  constructor(private service: MyService) {}
}
// ❌ Avoid - Creating instances
@Middleware()
export class MyMiddleware {
  use(context: ExecutionContext, next: () => Promise<any>) {
    const service = new MyService();  // Bad!
  }
}
5. Document Side Effects
/**
 * Adds request timing metadata to context.metadata.duration
 * Logs warning for requests > 1000ms
 */
@Middleware()
export class TimingMiddleware implements MiddlewareInterface {
  // ...
}
Next Steps
Pro Tip: Order your middleware thoughtfully - authentication before validation, logging before everything!