Skip to main content

Modules

A module is a plain function that receives a container and registers bindings into it. Modules are how you compose imperative configuration — split bindings into cohesive groups, import them by feature, and pass them to the container at construction time.

import { DiCaf, mod, type ContainerBindingOps } from '@caffeine-projects/dicaf'

function databaseModule(di: ContainerBindingOps) {
di.bind(Database).toClass(PostgresDatabase)
di.bind(UserRepository).toClass(UserRepository, [Database])
}

function emailModule(di: ContainerBindingOps) {
di.bind(Mailer).toClass(SmtpMailer)
}

const di = new DiCaf(databaseModule, emailModule)
await di.init()

Modules passed to the DiCaf constructor are applied immediately, before init() is called.

Naming a module

Use mod() to attach a debug name to any module. The name appears in error messages and hook events, making it easier to trace which module registered a failing binding.

import { mod } from '@caffeine-projects/dicaf'

const databaseModule = mod('database', (di) => {
di.bind(Database).toClass(PostgresDatabase)
})

Async modules

Modules can be async. The container awaits each async module during init().

const configModule = mod('config', async (di) => {
const config = await loadConfigFromRemote()
di.bind(AppConfig).toValue(config)
})

const di = new DiCaf(configModule)
await di.init()

Conditional registration

The preferred way to conditionally register a binding is .conditional() on the binder. The predicate receives a ConditionContext with access to the container's has() method, the binding key, and the binding config. It is evaluated once during init(), so the container is partially available:

import { type ContainerBindingOps } from '@caffeine-projects/dicaf'

function storageModule(di: ContainerBindingOps) {
di.bind(BlobStorage)
.toClass(S3BlobStorage)
.conditional(ctx => ctx.container.has(AppConfig))
}

Conditionals can be async:

di.bind(FeatureFlags)
.toClass(RemoteFeatureFlags)
.conditional(async ctx => {
const cfg = ctx.container.has(AppConfig)
return cfg && process.env.NODE_ENV === 'production'
})

Plain if/else also works when the condition is known at module-registration time (before init()):

function storageModule(di: ContainerBindingOps) {
if (process.env.NODE_ENV === 'test') {
di.bind(BlobStorage).toClass(InMemoryBlobStorage)
} else {
di.bind(BlobStorage).toClass(S3BlobStorage)
}
}

For profile-based or decorator-driven activation, see @Profile and @ConditionalOn.

Child containers

A child container inherits all bindings from its parent and can override or extend them without affecting the parent. This is useful for request-scoped setups or multi-tenant isolation.

const parent = new DiCaf(commonModule)
await parent.init()

const child = parent.newChild()
child.bind(TenantConfig).toValue(tenantConfig)
await child.init()

const svc = child.get(UserService) // resolved from parent

See the Container reference for full newChild() semantics.