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!