Modules
Defining a Module
Modules are classes decorated with @Module():
import { Module } from '@modularityjs/core';
@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 |
Lifecycle Hooks
Modules can implement lifecycle interfaces to run code at specific points during boot and shutdown:
import type { OnModuleInit, OnModuleReady } from '@modularityjs/core';
@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 | Collecting pool entries, building registries |
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/core';
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/core';
@Injectable()
class HealthService {
constructor(
@InjectPool(HealthIndicatorsPool)
private readonly indicators: HealthIndicator[],
) {}
}Pool entries support three forms: useClass, useValue, and useFactory.
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.
Configuration with forRoot()
Modules with configurable options expose a static forRoot() method:
@Module({
name: 'http',
providers: [HttpModuleConfig],
contracts: [HttpControllersPool],
})
class HttpModule {
static forRoot(config: Partial<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.
Optional Imports
Modules can declare optional dependencies that don't fail boot if missing:
@Module({
name: 'config',
imports: [{ module: ScopeModule, optional: true }],
})
class ConfigModule {}