Modules
Defining a Module
Modules are classes decorated with @Module():
import { Module } from '@modularityjs/modularity';
@Module({
name: 'my-module',
imports: [OtherModule],
providers: [MyService, MyConfig],
contracts: [MyServicePool],
preferences: [{ provide: AbstractService, useClass: ConcreteService }],
pools: [{ pool: SomePool, key: 'my-entry', useClass: MyPoolEntry }],
})
class MyModule {}Module Options
| Option | Description |
|---|---|
name | Unique module name (required) |
version | Optional semver version |
imports | Modules this module depends on |
providers | Classes to register in the DI container |
contracts | Pool tokens or abstract classes this module owns |
preferences | DI overrides (bind abstract to concrete) |
pools | Entries to contribute to pool collections |
overrides | Constructor argument overrides for arbitrary classes (see Overrides) |
Lifecycle Hooks
Modules can implement lifecycle interfaces to run code at specific points during boot and shutdown:
import type { OnModuleInit, OnModuleReady } from '@modularityjs/modularity';
@Module({ name: 'my-module' })
class MyModule implements OnModuleInit, OnModuleReady {
onInit(): void {
// Runs during createApp() — connect to databases, validate config
}
onReady(): void {
// Runs during app.start() — start HTTP server, begin processing
}
}Hook Order
| Hook | When | Use for |
|---|---|---|
afterLoad | After all modules loaded, before init | Framework-tier container.bind() and extension hook registrations |
onInit | During createApp() | Connecting to external services, validating config |
onReady | During app.start() | Starting servers, schedulers, consumer loops |
onShutdown | During app.shutdown() | Graceful stop of servers and consumers |
onDestroy | After shutdown | Closing connections, cleanup |
Hooks run in topological order (dependencies first for init, reverse for shutdown).
Pools
Pools allow modules to contribute entries to a shared collection discovered at boot time.
Defining a Pool
import { createPool } from '@modularityjs/modularity';
export const HealthIndicatorsPool = createPool<HealthIndicator, 'class'>(
'health:indicators',
);Contributing to a Pool
@Module({
name: 'my-health',
pools: [
{
pool: HealthIndicatorsPool,
key: 'database',
useClass: DbHealthIndicator,
},
],
})
class MyHealthModule {}Consuming a Pool
import { InjectPool } from '@modularityjs/modularity';
@Injectable()
class HealthService {
constructor(
@InjectPool(HealthIndicatorsPool)
private readonly indicators: HealthIndicator[],
) {}
}Pool entries support five forms:
useClass— register a class; the container instantiates and injects it.useValue— register a literal value for value-mode pools.useFactory— register the result of a factory function (with an optionalinjectlist of tokens).override— deep-merge aPartial<T>into an existing value-pool entry contributed under the samekeyupstream.remove: true— drop a previously contributed entry under the samekey, in either class- or value-mode pools.
Preferences
Preferences override DI bindings. The last module to declare a preference for a token wins:
@Module({
name: 'cache-redis',
imports: [CacheModule],
preferences: [{ provide: CacheService, useClass: RedisCacheService }],
})
class CacheRedisModule {}This is how drivers replace abstract contracts with concrete implementations.
Transitive Chaining
Preferences chain transitively. A preference can target any bound class — not just a declared contract — and the resolution follows the full chain at resolve time:
// Module A declares the contract
@Module({ name: 'cache', contracts: [CacheService] })
class CacheModule {}
// Module B provides a base driver
@Module({
name: 'cache-redis',
imports: [CacheModule],
providers: [RedisCacheService],
preferences: [{ provide: CacheService, useClass: RedisCacheService }],
})
class CacheRedisModule {}
// Module C wraps the driver with instrumentation
@Module({
name: 'cache-redis-instrumented',
imports: [CacheRedisModule],
providers: [InstrumentedRedisCacheService],
preferences: [
{ provide: RedisCacheService, useClass: InstrumentedRedisCacheService },
],
})
class CacheRedisInstrumentedModule {}When any service injects CacheService, the container follows the chain:
CacheService → RedisCacheService → InstrumentedRedisCacheServiceThis works because each preference creates a lazy alias that is resolved at injection time, not at registration time. This enables patterns like decorators, instrumentation wrappers, and multi-layer driver composition without modifying the original modules.
Configuration with forRoot()
Modules with configurable options expose a static forRoot() method:
@Module({
name: 'http',
providers: [HttpModuleConfig],
contracts: [HttpControllersPool],
})
class HttpModule {
static forRoot(config: DeepPartial<HttpModuleConfig>): ModuleDefinition {
return configureModule(HttpModule, HttpModuleConfig, config);
}
}
// Usage — contract configures, driver implements:
const modules = [HttpModule.forRoot({ port: 3000 }), HttpFastifyModule];The configureModule() helper merges partial config into the config class defaults and calls validate() on the config if the method exists.