PostProcessor
PostProcessor is Asena's component interception system. It lets you hook into the IoC bootstrap process to transform instances, collect metadata, or add cross-cutting behavior to components — similar to Spring's BeanPostProcessor or AOP interceptors.
When to Use PostProcessor
- Metadata Collection — Gather information from components at bootstrap (e.g., auto-generating API documentation)
- Instance Transformation — Wrap instances with Proxies for tracing, logging, or monitoring
- AOP Patterns — Add cross-cutting concerns without modifying individual components
TIP
If you just need initialization logic for a single component, use @PostConstruct instead. PostProcessor is for cross-cutting concerns that apply to multiple components.
Quick Start
import { PostProcessor } from '@asenajs/asena/decorators';
import type { ComponentPostProcessor } from '@asenajs/asena/ioc/types';
@PostProcessor()
export class LoggingPostProcessor implements ComponentPostProcessor {
postProcess<T>(instance: T, Class: any): T {
console.log(`Component initialized: ${Class.name}`);
return instance;
}
}Asena automatically discovers and registers the PostProcessor during bootstrap. The postProcess() method is called for every component that gets created.
ComponentPostProcessor Interface
import type { ComponentPostProcessor } from '@asenajs/asena/ioc/types';
export interface ComponentPostProcessor {
postProcess<T>(instance: T, Class: any): T | Promise<T>;
}| Parameter | Type | Description |
|---|---|---|
instance | T | The fully initialized component instance (after DI + PostConstruct) |
Class | any | The original class constructor (for reading metadata) |
| Returns | T | Promise<T> | The processed instance. Return null/undefined to keep the original |
@PostProcessor Decorator
import { PostProcessor } from '@asenajs/asena/decorators';
@PostProcessor() // Default
@PostProcessor({ name: 'MyProcessor' }) // NamedAccepts optional ComponentParams:
| Parameter | Type | Default | Description |
|---|---|---|---|
name | string | Class name | Component name for IoC |
Lifecycle
PostProcessor runs at a specific point in the component initialization chain:
Constructor → @Inject (DI) → @PostConstruct → postProcess()- Component is instantiated (
new) - Dependencies are injected (
@Inject) - PostConstruct methods are called (
@PostConstruct) - PostProcessors run — instance is fully initialized at this point
Execution Order
If multiple PostProcessors are registered, they execute in FIFO order (first registered, first executed). Each processor's output becomes the next processor's input (chaining).
Two Modes of Operation
Mode 1: Instance Transformation
Return a modified or wrapped instance. Useful for tracing, monitoring, or adding behavior.
import { PostProcessor } from '@asenajs/asena/decorators';
import type { ComponentPostProcessor } from '@asenajs/asena/ioc/types';
@PostProcessor()
export class TracingPostProcessor implements ComponentPostProcessor {
postProcess<T>(instance: T, Class: any): T {
return new Proxy(instance as object, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
if (typeof value === 'function') {
return function (...args: any[]) {
console.log(`[TRACE] ${Class.name}.${String(prop)}() called`);
return value.apply(target, args);
};
}
return value;
},
}) as T;
}
}Mode 2: Metadata Collection
Collect information from components without modifying them. Return the original instance unchanged.
import { PostProcessor } from '@asenajs/asena/decorators';
import type { ComponentPostProcessor } from '@asenajs/asena/ioc/types';
@PostProcessor()
export class ComponentRegistryPostProcessor implements ComponentPostProcessor {
private readonly registry: Map<string, any> = new Map();
postProcess<T>(instance: T, Class: any): T {
// Collect metadata — don't modify the instance
this.registry.set(Class.name, {
name: Class.name,
methods: Object.getOwnPropertyNames(Class.prototype),
});
return instance; // Return unchanged
}
getRegistry() {
return this.registry;
}
}Real-World Example: OpenAPI PostProcessor
The @asenajs/asena-openapi package uses a PostProcessor to automatically generate OpenAPI specs from your existing controllers and validators. Here's a simplified version:
import { PostProcessor, PostConstruct } from '@asenajs/asena/decorators';
import { Inject } from '@asenajs/asena/decorators/ioc';
import type { ComponentPostProcessor } from '@asenajs/asena/ioc/types';
import { extractControllerRouteInfo, isController } from '@asenajs/asena/utils';
@PostProcessor()
export class OpenApiPostProcessor implements ComponentPostProcessor {
@Inject(ICoreServiceNames.ASENA_ADAPTER)
private adapter: AsenaAdapter<any, any>;
private controllers: { instance: any; Class: any }[] = [];
private validators = new Map<string, any>();
@PostConstruct()
public onInit(): void {
// Register the /openapi endpoint during initialization
this.adapter.registerRoute({
method: 'GET',
path: '/openapi',
handler: async (context: any) => {
const spec = await this.generateSpec();
return context.send(spec);
},
});
}
public postProcess<T>(instance: T, Class: any): T {
// Mode 2: Collect controllers and validators
if (isController(Class)) {
this.controllers.push({ instance, Class });
}
if (this.isValidator(Class)) {
this.validators.set(Class.name, instance);
}
return instance; // Return unchanged
}
private async generateSpec() {
// Generate OpenAPI 3.1 spec from collected controllers + validators
for (const { instance, Class } of this.controllers) {
const { basePath, routes } = extractControllerRouteInfo(instance);
// ... build spec from route metadata and validator schemas
}
}
}How it works:
@PostConstructregisters the/openapiGET endpoint on the adapterpostProcess()is called for every component — it selectively collects controllers and validators- When
/openapiis requested, the spec is lazily generated from collected metadata - The original instances are never modified
Full Source
See the full implementation: OpenApiPostProcessor.ts on GitHub
Bootstrap Priority
PostProcessors are registered in a special Phase A during bootstrap, before all other components:
- Phase A: PostProcessor classes and their dependencies are created. PostProcessors are NOT post-processed themselves (prevents infinite loops)
- Phase B: All remaining components are created. Post-processing is now active, so every component goes through
postProcess()
This guarantees that PostProcessors are ready before any user component is created.
WARNING
Dependencies of PostProcessors (services injected via @Inject) are also created in Phase A and are not post-processed. Keep PostProcessor dependencies minimal.
Dependency Injection in PostProcessors
PostProcessors are full IoC components — you can inject other services:
@PostProcessor()
export class MetricsPostProcessor implements ComponentPostProcessor {
@Inject(ICoreServiceNames.ASENA_ADAPTER)
private adapter: AsenaAdapter<any, any>;
@Inject('MetricsService')
private metrics: MetricsService;
postProcess<T>(instance: T, Class: any): T {
this.metrics.trackComponent(Class.name);
return instance;
}
}@PostConstruct vs @PostProcessor
| Aspect | @PostConstruct | @PostProcessor |
|---|---|---|
| Type | Method decorator | Class decorator |
| Scope | Single component | All components |
| When | After DI, before post-processing | After DI + PostConstruct |
| Purpose | Component initialization | Cross-cutting concerns |
| Can transform | No (runs on self) | Yes (returns modified instance) |
| Use case | Setup resources, validate config | Tracing, metadata collection, AOP |
Best Practices
1. Use Mode 2 for Most Cases
// ✅ Good: Collect metadata, return unchanged
postProcess<T>(instance: T, Class: any): T {
if (isController(Class)) {
this.controllers.push(Class.name);
}
return instance;
}
// ⚠️ Careful: Only transform when truly needed (tracing, monitoring)
postProcess<T>(instance: T, Class: any): T {
return new Proxy(instance, { /* ... */ });
}2. Be Selective
// ✅ Good: Only process relevant components
postProcess<T>(instance: T, Class: any): T {
if (!isController(Class)) return instance; // Skip non-controllers
// ... process controller
return instance;
}
// ❌ Bad: Processing every single component
postProcess<T>(instance: T, Class: any): T {
// Heavy processing on ALL components
return this.expensiveOperation(instance);
}3. Keep Dependencies Minimal
// ✅ Good: Minimal dependencies (remember: deps aren't post-processed)
@PostProcessor()
export class SimpleProcessor implements ComponentPostProcessor {
postProcess<T>(instance: T, Class: any): T { /* ... */ }
}
// ❌ Bad: Many dependencies (all created in Phase A, not post-processed)
@PostProcessor()
export class HeavyProcessor implements ComponentPostProcessor {
@Inject('ServiceA') private a: ServiceA;
@Inject('ServiceB') private b: ServiceB;
@Inject('ServiceC') private c: ServiceC;
// ...
}Related Documentation
- Services - Service layer architecture
- Dependency Injection - IoC container
- OpenAPI - PostProcessor in action
- Configuration - Server configuration
Next Steps:
- See PostProcessor in action with OpenAPI
- Learn about Dependency Injection
- Explore Services