zmdbzero-maintenance data layer
Docs Benchmarks Anti-patterns OpenAPI
Docs / Server framework

Modules & ProvidersSupported

Organize controllers and providers into composable modules over the DI container — the NestJS @Module analogue, declared and validated statically at compile time (no per-request graph walk or reflection).

Declaring a module#

import { createToken } from '@zmdb/app/di';
import { Module } from '@zmdb/app/modules';

class Clock {
  now() {
    return Date.now();
  }
}
const ClockToken = createToken<Clock>('Clock');

@Module({
  providers: [{ token: ClockToken, useValue: new Clock() }],
  exports: [ClockToken], // documents intended visibility; not enforced yet
})
class SharedModule {}

@Module({
  imports: [SharedModule], // ClockToken enters the shared container
  controllers: [TimeController], // built through the container
  providers: [{ token: CounterToken, useFactory: c => makeCounter(), scope: 'transient' }],
})
class AppModule {}

Provider kinds & scopes#

providershaperesolution
value{ token, useValue }returns the bound value
factory (singleton){ token, useFactory }runs once, then cached
factory (transient){ token, useFactory, scope: 'transient' }runs every resolve

Compiling the graph#

import { compileModule } from '@zmdb/app/modules';

const { container, controllers, lazy } = compileModule(AppModule);
// providers registered (imports resolved first), controllers built with their
// @Inject-ed dependencies satisfied. Import cycles throw.

lazy is empty for an all-eager graph or contains one per-app handle for each lazy subtree.

⚠️ Warning

exports records intent but does not enforce visibility. The graph currently uses one flat container, while duplicate provider tokens across modules are refused at startup.

Lazy imports#

import { lazy, Module } from '@zmdb/app/modules';

@Module({
  imports: [SharedModule, lazy(AdminModule)],
})
class AppModule {}

The complete graph is validated during compileModule, but AdminModule is constructed only when one of its routes is first requested or its app.lazy handle is loaded. Eager remains the default. See Lazy-Loading Modules for concurrency, lifecycle and failure behavior.

Design notes#