Interceptors
Interceptors allow you to intercept and modify the dependency resolution process. They can be used for caching, logging, validation, retry logic, and more.
Two Styles of Interceptors
You can define interceptors in two ways: class-based (traditional) or functional (modern, like Angular 15+).
Class-based Interceptor
import type { Interceptor, InjectionContext } from '@noneforge/ioc';
class LoggingInterceptor implements Interceptor {
intercept<T>(context: InjectionContext, next: () => T): T {
console.log(`Resolving: ${String(context.token)}`);
return next();
}
}
// Usage
container.addProvider({
provide: SERVICE,
useClass: MyService,
interceptors: [new LoggingInterceptor()],
});Functional Interceptor
import type { InterceptorFn } from '@noneforge/ioc';
const loggingInterceptor: InterceptorFn = (context, next) => {
console.log(`Resolving: ${String(context.token)}`);
return next();
};
// Usage
container.addProvider({
provide: SERVICE,
useClass: MyService,
interceptors: [loggingInterceptor],
});Mixing Both Styles
You can freely mix class-based and functional interceptors:
container.addProvider({
provide: SERVICE,
useClass: MyService,
interceptors: [
new CachingInterceptor(), // class
loggingInterceptor, // function variable
(ctx, next) => { // inline function
console.log('Resolving:', ctx.token);
return next();
},
],
});Interceptor Interface
// Class-based (with generics for type safety)
interface Interceptor {
intercept<T>(
context: InjectionContext,
next: () => T | Promise<T>
): T | Promise<T>;
}
// Functional (simpler, uses unknown)
type InterceptorFn = (
context: InjectionContext,
next: () => unknown
) => unknown;
// Union type (accepts both)
type InterceptorLike = Interceptor | InterceptorFn;The intercept method/function receives:
context- Information about the current resolution (token, container, metadata)next- Function to call the next interceptor or resolve the dependency
Built-in Interceptors
CachingInterceptor
Caches resolved instances with optional TTL (Time To Live):
import { Container, CachingInterceptor, InjectionToken } from '@noneforge/ioc';
const EXPENSIVE_SERVICE = new InjectionToken<ExpensiveService>('EXPENSIVE');
const container = new Container();
container.addProvider({
provide: EXPENSIVE_SERVICE,
useFactory: () => {
console.log('Creating expensive service...');
return new ExpensiveService();
},
interceptors: [new CachingInterceptor()], // Default: no expiration
});
// First call creates the instance
const service1 = container.get(EXPENSIVE_SERVICE);
// "Creating expensive service..."
// Second call uses cached instance
const service2 = container.get(EXPENSIVE_SERVICE);
// No log - cached!
console.log(service1 === service2); // trueWith TTL
container.addProvider({
provide: EXPENSIVE_SERVICE,
useFactory: () => new ExpensiveService(),
interceptors: [
new CachingInterceptor(60000), // Cache for 60 seconds
],
});LoggingInterceptor
Logs resolution events with timing information:
import { Container, LoggingInterceptor, InjectionToken } from '@noneforge/ioc';
// Default logger (console)
const loggingInterceptor = new LoggingInterceptor();
// Custom logger
interface Logger {
debug?(message: string, ...args: unknown[]): void;
error?(message: string, error?: Error): void;
}
const customLogger: Logger = {
debug: (msg, ...args) => console.log(`[DEBUG] ${msg}`, ...args),
error: (msg, err) => console.error(`[ERROR] ${msg}`, err),
};
const customLoggingInterceptor = new LoggingInterceptor(customLogger);
container.addProvider({
provide: UserService,
useClass: UserService,
interceptors: [new LoggingInterceptor()],
});
container.get(UserService);
// Logs:
// Resolving: UserService
// Resolved UserService in 2msRetryInterceptor
Automatically retries failed resolutions with exponential backoff:
import { Container, RetryInterceptor, InjectionToken } from '@noneforge/ioc';
const REMOTE_CONFIG = new InjectionToken<RemoteConfig>('REMOTE_CONFIG');
container.addProvider({
provide: REMOTE_CONFIG,
useAsync: async () => {
const response = await fetch('https://config.example.com');
if (!response.ok) {
throw new Error('Failed to fetch config');
}
return response.json();
},
interceptors: [
new RetryInterceptor(3, 1000), // 3 retries, 1 second initial delay
],
});
// Will retry up to 3 times with exponential backoff
const config = await container.getAsync(REMOTE_CONFIG);Retry Parameters
new RetryInterceptor(
maxRetries, // Maximum number of retries (default: 3)
delay // Initial delay in ms (default: 1000)
);
// Delay doubles after each retry (exponential backoff)
// 1s -> 2s -> 4s -> ...ValidationInterceptor
Validates resolved values against a custom validator:
import { Container, ValidationInterceptor, InjectionToken } from '@noneforge/ioc';
interface User {
id: number;
email: string;
}
const USER = new InjectionToken<User>('USER');
container.addProvider({
provide: USER,
useValue: { id: 1, email: 'test@example.com' },
interceptors: [
new ValidationInterceptor(
(value) => {
const user = value as User;
return user.id > 0 && user.email.includes('@');
},
'User must have valid id and email'
),
],
});
container.get(USER); // Works
// Invalid value throws ValidationError
container.addProvider({
provide: USER,
useValue: { id: -1, email: 'invalid' },
interceptors: [
new ValidationInterceptor(
(value) => (value as User).id > 0,
'User id must be positive'
),
],
});
container.get(USER); // Throws ValidationError!Creating Custom Interceptors
Basic Custom Interceptor
import type { Interceptor, InjectionContext } from '@noneforge/ioc';
class TimingInterceptor implements Interceptor {
intercept<T>(context: InjectionContext, next: () => T): T {
const start = performance.now();
const result = next();
const duration = performance.now() - start;
console.log(`Resolved ${String(context.token)} in ${duration.toFixed(2)}ms`);
return result;
}
}Async Custom Interceptor
class AsyncTimingInterceptor implements Interceptor {
async intercept<T>(
context: InjectionContext,
next: () => T | Promise<T>
): Promise<T> {
const start = performance.now();
const result = await next();
const duration = performance.now() - start;
console.log(`Resolved ${String(context.token)} in ${duration.toFixed(2)}ms`);
return result;
}
}Conditional Interceptor
class DebugInterceptor implements Interceptor {
constructor(private debug: boolean) {}
intercept<T>(context: InjectionContext, next: () => T): T {
if (this.debug) {
console.log(`Resolving: ${String(context.token)}`);
console.log(`Depth: ${context.depth}`);
console.log(`Path: ${context.path.map(String).join(' -> ')}`);
}
return next();
}
}Result-Modifying Interceptor
class WrappingInterceptor implements Interceptor {
intercept<T>(context: InjectionContext, next: () => T): T {
const result = next();
// Wrap the result
if (typeof result === 'object' && result !== null) {
return new Proxy(result as object, {
get(target, prop) {
console.log(`Accessing ${String(prop)}`);
return Reflect.get(target, prop);
},
}) as T;
}
return result;
}
}Error-Handling Interceptor
class FallbackInterceptor<T> implements Interceptor {
constructor(private fallback: T) {}
intercept<U>(context: InjectionContext, next: () => U): U {
try {
return next();
} catch (error) {
console.warn(`Failed to resolve ${String(context.token)}, using fallback`);
return this.fallback as unknown as U;
}
}
}Using inject() in Functional Interceptors
Functional interceptors run within an injection context, so you can use inject() to resolve dependencies:
import { inject, type InterceptorFn } from '@noneforge/ioc';
const authInterceptor: InterceptorFn = (context, next) => {
// inject() works because we're in an injection context
const authService = inject(AuthService);
if (!authService.isAuthenticated()) {
throw new Error('Not authenticated');
}
return next();
};
const loggingInterceptor: InterceptorFn = (context, next) => {
const logger = inject(Logger);
logger.debug(`Resolving: ${String(context.token)}`);
return next();
};This is similar to how Angular's functional interceptors work with inject().
Helper Functions
Helper functions make it easier to create common interceptor patterns.
createInterceptor
Create an interceptor from pre/post hooks:
import { createInterceptor } from '@noneforge/ioc';
const timingInterceptor = createInterceptor({
pre: (context) => {
context.metadata.set('startTime', Date.now());
},
post: (context, result) => {
const duration = Date.now() - (context.metadata.get('startTime') as number);
console.log(`Resolution took ${duration}ms`);
return result;
},
});
// Only pre hook
const preOnlyInterceptor = createInterceptor({
pre: (context) => console.log('Before resolution'),
});
// Only post hook
const transformInterceptor = createInterceptor({
post: (context, result: string) => result.toUpperCase(),
});composeInterceptors
Combine multiple interceptors into one:
import { composeInterceptors, LoggingInterceptor, CachingInterceptor } from '@noneforge/ioc';
const combined = composeInterceptors(
new LoggingInterceptor(),
new CachingInterceptor(),
(ctx, next) => {
console.log('Custom logic');
return next();
},
);
// Use as single interceptor
container.addProvider({
provide: SERVICE,
useClass: MyService,
interceptors: [combined],
});when
Create a conditional interceptor that only runs when predicate returns true:
import { when } from '@noneforge/ioc';
// Only log in debug mode
const debugInterceptor = when(
(ctx) => ctx.metadata.get('debug') === true,
(ctx, next) => {
console.log('Debug:', ctx.token);
return next();
},
);
// Only intercept specific tokens
const specificTokenInterceptor = when(
(ctx) => ctx.token === UserService,
(ctx, next) => {
console.log('Intercepting UserService');
return next();
},
);
// Combine with class interceptors
const conditionalCache = when(
(ctx) => ctx.metadata.get('useCache') !== false,
new CachingInterceptor(),
);Using Interceptors
Provider-Level Interceptors
Apply interceptors to specific providers:
container.addProvider({
provide: UserService,
useClass: UserService,
interceptors: [
new LoggingInterceptor(),
new CachingInterceptor(60000),
],
});Multiple Interceptors
Interceptors are executed in order:
container.addProvider({
provide: SERVICE,
useClass: Service,
interceptors: [
new LoggingInterceptor(), // 1st: logs start
new CachingInterceptor(), // 2nd: checks cache
new ValidationInterceptor(), // 3rd: validates result
],
});
// Execution order:
// 1. LoggingInterceptor.intercept() -> calls next()
// 2. CachingInterceptor.intercept() -> calls next()
// 3. ValidationInterceptor.intercept() -> calls next()
// 4. Actual resolution
// 3. ValidationInterceptor validates result
// 2. CachingInterceptor caches result
// 1. LoggingInterceptor logs completionInjectionContext
The context object provides information about the current resolution:
interface InjectionContext {
// The container performing resolution
container: ContainerLike;
// The token being resolved
token?: Token;
// Request ID for request-scoped services
requestId?: string | symbol;
// Custom metadata
metadata: Map<string, unknown>;
// Resolution depth (for nested dependencies)
depth: number;
// Resolution path (for cycle detection)
path: Token[];
// Resolution strategy
strategy: ResolutionStrategy;
}Using Context in Interceptors
class ContextAwareInterceptor implements Interceptor {
intercept<T>(context: InjectionContext, next: () => T): T {
// Check resolution depth
if (context.depth > 10) {
console.warn('Deep dependency tree detected');
}
// Check for specific tokens
if (context.token === SensitiveService) {
console.log('Accessing sensitive service');
}
// Access custom metadata
const userId = context.metadata.get('userId');
if (userId) {
console.log(`Resolution for user: ${userId}`);
}
return next();
}
}InterceptorChain
For advanced use cases, you can use InterceptorChain directly:
import { InterceptorChain } from '@noneforge/ioc';
const chain = new InterceptorChain();
chain.add(new LoggingInterceptor());
chain.add(new CachingInterceptor());
// Synchronous execution
const result = chain.execute(context, () => createInstance());
// Asynchronous execution
const asyncResult = await chain.executeAsync(context, async () => {
return await createInstanceAsync();
});Best Practices
- Keep interceptors focused - Each interceptor should do one thing
- Order matters - Put logging first, caching second, validation last
- Handle errors gracefully - Don't let interceptor errors crash the app
- Use async when needed - Return promises for async operations
- Avoid side effects - Interceptors should be predictable
Common Interceptor Patterns
// Logging + Caching + Validation
const standardInterceptors = [
new LoggingInterceptor(logger),
new CachingInterceptor(TTL),
new ValidationInterceptor(validator),
];
// Retry + Logging for external services
const externalServiceInterceptors = [
new RetryInterceptor(3, 1000),
new LoggingInterceptor(logger),
];
// Debug mode
const debugInterceptors = process.env.DEBUG
? [new LoggingInterceptor(), new TimingInterceptor()]
: [];Runnable Example
See examples/interceptors for a complete runnable example demonstrating all interceptor patterns.
Next Steps
- Plugin System - Extend container with plugins
- Testing - Testing with interceptors
- Middleware - Container-level middleware
- API Reference - Complete interceptor API