Middleware
Request processing and authentication middleware
Middleware functions execute before and after tool, resource, and prompt handlers. They enable cross-cutting concerns such as logging, timing, authentication verification, request transformation, and error handling.
Middleware follows the "onion model" where each layer wraps around the next, with the handler at the center.
Creating Middleware
A middleware is a class with a use() method. It receives the execution context and a next function, runs code before and after calling next(), and returns the result.
Basic Middleware
Middleware implements the MiddlewareInterface:
import { Middleware, MiddlewareInterface, ExecutionContext } from '@nitrostack/core';
@Middleware()
export class LoggingMiddleware implements MiddlewareInterface {
async use(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
const startTime = Date.now();
const toolName = context.toolName || 'unknown';
context.logger.info(`Request started: ${toolName}`, {
requestId: context.requestId,
timestamp: new Date().toISOString()
});
try {
// Execute next middleware or handler
const result = await next();
const duration = Date.now() - startTime;
context.logger.info(`Request completed: ${toolName}`, {
requestId: context.requestId,
duration,
success: true
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
context.logger.error(`Request failed: ${toolName}`, {
requestId: context.requestId,
duration,
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
}Middleware Interface
Every middleware implements this one method. Call next() to continue the pipeline; return without calling it to stop the call.
interface MiddlewareInterface {
use(context: ExecutionContext, next: () => Promise<any>): Promise<any>;
}Parameters:
context: The execution context with auth, logger, and metadatanext: Function to call the next middleware or handler
Return: The result from the handler (possibly transformed)
Using Middleware
Attach middleware with @UseMiddleware, on a single handler or on several.
On Individual Methods
Put @UseMiddleware on the handler it should wrap.
import { ToolDecorator as Tool, UseMiddleware, ExecutionContext } from '@nitrostack/core';
import { LoggingMiddleware } from './middleware/logging.middleware.js';
export class ProductTools {
@Tool({ name: 'get_product' })
@UseMiddleware(LoggingMiddleware)
async getProduct(input: { productId: string }, ctx: ExecutionContext) {
return this.productService.findById(input.productId);
}
}Multiple Middleware
List several middleware in one decorator. They run in the order listed.
import { TimingMiddleware } from './middleware/timing.middleware.js';
import { ValidationMiddleware } from './middleware/validation.middleware.js';
@Tool({ name: 'create_order' })
@UseMiddleware(LoggingMiddleware, TimingMiddleware, ValidationMiddleware)
async createOrder(input: CreateOrderInput, ctx: ExecutionContext) {
return this.orderService.create(input);
}Execution Order
Middleware executes in declaration order, forming a pipeline:
Order Matters
Put middleware that others depend on first. A request ID, for example, should exist before anything logs.
// Recommended order:
@UseMiddleware(
RequestIdMiddleware, // 1. Generate request ID first
LoggingMiddleware, // 2. Log with request ID
AuthenticationMiddleware, // 3. Verify authentication
ValidationMiddleware // 4. Validate input
)Common Patterns
These middleware solve problems most servers have.
Request ID Generation
Give every call an ID you can search for in your logs.
@Middleware()
export class RequestIdMiddleware implements MiddlewareInterface {
private counter = 0;
async use(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
const timestamp = Date.now().toString(36);
const random = Math.random().toString(36).substring(2, 8);
const sequence = (++this.counter).toString(36).padStart(4, '0');
const requestId = `req_${timestamp}_${random}_${sequence}`;
// Store in metadata for downstream use
if (context.metadata) {
context.metadata.requestId = requestId;
context.metadata.requestTimestamp = new Date().toISOString();
}
return next();
}
}Performance Timing
Measure how long each call takes, and warn when one is slow.
@Middleware()
export class TimingMiddleware implements MiddlewareInterface {
private static readonly SLOW_THRESHOLD_MS = 1000;
async use(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
const startTime = process.hrtime.bigint();
try {
const result = await next();
this.recordTiming(context, startTime);
return result;
} catch (error) {
this.recordTiming(context, startTime);
throw error;
}
}
private recordTiming(context: ExecutionContext, startTime: bigint): void {
const endTime = process.hrtime.bigint();
const durationMs = Number(endTime - startTime) / 1_000_000;
if (context.metadata) {
context.metadata.executionTimeMs = durationMs;
}
if (durationMs > TimingMiddleware.SLOW_THRESHOLD_MS) {
context.logger.warn('Slow request detected', {
toolName: context.toolName,
duration: durationMs,
threshold: TimingMiddleware.SLOW_THRESHOLD_MS
});
}
}
}Error Handling
Catch errors from everything inside the middleware, log them with context, and rethrow.
@Middleware()
export class ErrorHandlingMiddleware implements MiddlewareInterface {
async use(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
try {
return await next();
} catch (error) {
// Log error with context
context.logger.error('Request error', {
toolName: context.toolName,
requestId: context.metadata?.requestId,
error: this.serializeError(error)
});
// Transform error for client
if (error instanceof ValidationError) {
return {
error: true,
code: 'VALIDATION_ERROR',
message: error.message,
details: error.details
};
}
if (error instanceof NotFoundError) {
return {
error: true,
code: 'NOT_FOUND',
message: error.message
};
}
// Unknown errors
return {
error: true,
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred'
};
}
}
private serializeError(error: unknown): Record<string, unknown> {
if (error instanceof Error) {
return {
name: error.name,
message: error.message,
stack: error.stack
};
}
return { value: String(error) };
}
}Request Context Enrichment
Add information, such as the environment name, to the context so later stages can use it.
@Middleware()
export class ContextEnrichmentMiddleware implements MiddlewareInterface {
async use(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
if (context.metadata) {
// Add environment information
context.metadata.environment = process.env.NODE_ENV || 'development';
context.metadata.serverVersion = process.env.APP_VERSION || '1.0.0';
// Add user context if authenticated
if (context.auth?.subject) {
context.metadata.userId = context.auth.subject;
context.metadata.userRoles = context.auth.roles || [];
}
}
return next();
}
}Conditional Processing
Skip work for calls that don't need it, such as health checks.
@Middleware()
export class ConditionalMiddleware implements MiddlewareInterface {
private readonly skipTools = ['health_check', 'ping', 'version'];
async use(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
// Skip middleware for certain tools
if (this.skipTools.includes(context.toolName || '')) {
return next();
}
// Apply middleware logic only for non-skipped tools
context.logger.info('Processing request', {
toolName: context.toolName
});
return next();
}
}Metrics Collection
Count calls and failures per tool. Making the middleware injectable lets other services read the numbers.
import { Injectable } from '@nitrostack/core';
@Injectable()
@Middleware()
export class MetricsMiddleware implements MiddlewareInterface {
private requestCounts = new Map<string, number>();
private errorCounts = new Map<string, number>();
private totalDuration = new Map<string, number>();
async use(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
const toolName = context.toolName || 'unknown';
const startTime = Date.now();
// Increment request count
this.requestCounts.set(toolName, (this.requestCounts.get(toolName) || 0) + 1);
try {
const result = await next();
// Record duration
const duration = Date.now() - startTime;
this.totalDuration.set(toolName, (this.totalDuration.get(toolName) || 0) + duration);
return result;
} catch (error) {
// Increment error count
this.errorCounts.set(toolName, (this.errorCounts.get(toolName) || 0) + 1);
throw error;
}
}
getMetrics(): Record<string, unknown> {
const tools: Record<string, unknown> = {};
for (const [tool, count] of this.requestCounts) {
tools[tool] = {
requests: count,
errors: this.errorCounts.get(tool) || 0,
avgDuration: (this.totalDuration.get(tool) || 0) / count
};
}
return { tools };
}
}Dependency Injection
Middleware can use dependency injection:
import { Injectable, Middleware, MiddlewareInterface } from '@nitrostack/core';
@Injectable()
@Middleware()
export class AuditMiddleware implements MiddlewareInterface {
constructor(
private auditService: AuditService,
private configService: ConfigService
) {}
async use(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
const shouldAudit = this.configService.get('ENABLE_AUDIT_LOG', true);
if (!shouldAudit) {
return next();
}
const startTime = Date.now();
const auditEntry = {
toolName: context.toolName,
userId: context.auth?.subject,
requestId: context.metadata?.requestId,
timestamp: new Date().toISOString()
};
try {
const result = await next();
await this.auditService.log({
...auditEntry,
status: 'success',
duration: Date.now() - startTime
});
return result;
} catch (error) {
await this.auditService.log({
...auditEntry,
status: 'error',
duration: Date.now() - startTime,
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
}