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 @OnStart 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 component instance with every @Inject / @Strategy field populated. Its @OnStart has not run yet |
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) → @Strategy → postProcess() … later, from server.start() → @OnStart- Component is instantiated (
new) - Dependencies are injected (
@Inject) - Strategy arrays are resolved (
@Strategy) - PostProcessors run — every injected field is populated at this point
- Much later, once the whole graph exists,
@OnStarthooks run fromserver.start()
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).
Changed in 0.10.0
Up to 0.9.x @PostConstruct ran before postProcess(). Start hooks now run after it, and against the post-processed instance — a processor that returns a proxy hands the hook the same object every other component was injected with. A processor that relied on the wrapped instance already having run its own initialization no longer can; do that work in postProcess() itself, or in the processor's own @OnStart (see below).
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 } from '@asenajs/asena/decorators';
import { OnStart } from '@asenajs/asena/decorators/ioc';
import { Inject } from '@asenajs/asena/decorators/ioc';
import type { ComponentPostProcessor } from '@asenajs/asena/ioc/types';
import { extractControllerRouteInfo, isController, isValidator } 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>();
@OnStart()
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 (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:
@OnStartregisters 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.
Phase A keeps the old start-hook timing
Phase B components have their @OnStart deferred to server.start(). Phase A components — the processors and their dependencies — run theirs at construction, as they always did, because postProcess() reads state the processor's own start hook sets up. Deferring it would wrap every Phase B component against an uninitialised processor.
Two consequences: a processor's @OnStart cannot reach the microservice transports (they are not connected yet), and because it counts as started from construction, its @OnStop runs on server.stop() even if start() was never called.
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;
}
}@OnStart vs @PostProcessor
| Aspect | @OnStart | @PostProcessor |
|---|---|---|
| Type | Method decorator | Class decorator |
| Scope | Single component | All components |
| When | From server.start(), once the whole graph exists | At construction, right after DI |
| Counterpart | @OnStop | None |
| 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
- Component Lifecycle -
@OnStart/@OnStopand why PostProcessors start earlier - OpenAPI - PostProcessor in action
- Configuration - Server configuration
Next Steps:
- See PostProcessor in action with OpenAPI
- Learn about Dependency Injection
- Explore Services