Skip to content

Modules

Defining a Module

Modules are classes decorated with @Module():

typescript
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

OptionDescription
nameUnique module name (required)
versionOptional semver version
importsModules this module depends on
providersClasses to register in the DI container
contractsPool tokens or abstract classes this module owns
preferencesDI overrides (bind abstract to concrete)
poolsEntries to contribute to pool collections
overridesConstructor argument overrides for arbitrary classes (see Overrides)

Lifecycle Hooks

Modules can implement lifecycle interfaces to run code at specific points during boot and shutdown:

typescript
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

HookWhenUse for
afterLoadAfter all modules loaded, before initFramework-tier container.bind() and extension hook registrations
onInitDuring createApp()Connecting to external services, validating config
onReadyDuring app.start()Starting servers, schedulers, consumer loops
onShutdownDuring app.shutdown()Graceful stop of servers and consumers
onDestroyAfter shutdownClosing 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

typescript
import { createPool } from '@modularityjs/modularity';

export const HealthIndicatorsPool = createPool<HealthIndicator, 'class'>(
  'health:indicators',
);

Contributing to a Pool

typescript
@Module({
  name: 'my-health',
  pools: [
    {
      pool: HealthIndicatorsPool,
      key: 'database',
      useClass: DbHealthIndicator,
    },
  ],
})
class MyHealthModule {}

Consuming a Pool

typescript
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 optional inject list of tokens).
  • override — deep-merge a Partial<T> into an existing value-pool entry contributed under the same key upstream.
  • remove: true — drop a previously contributed entry under the same key, in either class- or value-mode pools.

Preferences

Preferences override DI bindings. The last module to declare a preference for a token wins:

typescript
@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:

typescript
// 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 → InstrumentedRedisCacheService

This 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:

typescript
@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.