Skip to content

Modules

Defining a Module

Modules are classes decorated with @Module():

typescript
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

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

Lifecycle Hooks

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

typescript
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

HookWhenUse for
afterLoadAfter all modules loaded, before initCollecting pool entries, building registries
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/core';

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

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

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

typescript
@Module({
  name: 'config',
  imports: [{ module: ScopeModule, optional: true }],
})
class ConfigModule {}