Dependency Substitution
Beyond automatic constructor injection, Moost lets you control how dependencies are provided and replace one class with another — useful for testing, feature toggles, and runtime configuration.
@Provide
@Provide binds a factory function to a class type or string key. The provided instance propagates down the dependency tree — the class itself and every dependency it creates (directly or transitively) will see this instance when they ask for it. A deeper @Provide for the same type overrides the parent one for that subtree.
This is what makes @Provide different from just calling new in a constructor: you're configuring how a dependency resolves for an entire branch of the object graph, not just one consumer.
Class-based — provide a specific instance for the entire subtree:
import { Provide, Controller } from 'moost'
@Provide(Logger, () => new Logger('api'))
@Controller('api')
class ApiController {
// ApiController, its dependencies, and their dependencies
// all receive this Logger instance when they inject Logger
constructor(private userService: UserService) {}
}Here UserService and anything UserService depends on will all get the Logger('api') instance — without any of them knowing about the provide. A different controller could provide Logger('admin') and its subtree would get that one instead.
String key — differentiate multiple instances of the same type:
@Provide('primary-db', () => new Database(primaryConfig))
@Provide('analytics-db', () => new Database(analyticsConfig))
class ReportController {
constructor(
@Inject('primary-db') private primary: Database,
@Inject('analytics-db') private analytics: Database,
) {}
}@Inject
Explicitly specify which dependency to inject by class type or string key:
import { Inject } from 'moost'
class MyController {
constructor(@Inject('cache-store') private cache: CacheStore) {}
}Use @Inject when the constructor parameter type alone isn't enough to identify the dependency — typically with string-keyed provides.
Global Provide Registry
Instead of scattering @Provide across classes, centralize dependency definitions on the app instance:
import { createProvideRegistry } from 'moost'
app.setProvideRegistry(createProvideRegistry(
[DatabaseConnection, () => new DatabaseConnection(config)],
['cache-store', () => new RedisCache()],
))This keeps configuration in one place, making it easier to swap implementations across the whole app.
@Replace
Override one class with another globally. Every injection point expecting the original class receives the replacement instead.
Via decorator:
import { Replace } from 'moost'
@Replace(EmailService, MockEmailService)
class TestController {
// EmailService injections now resolve to MockEmailService
}Via registry:
import { createReplaceRegistry } from 'moost'
app.setReplaceRegistry(createReplaceRegistry(
[EmailService, MockEmailService],
[PaymentGateway, TestPaymentGateway],
))This is especially useful for:
- Testing — inject mocks without changing consumer code
- Feature toggles — swap implementations at startup
- Third-party controllers — override dependencies you can't modify directly
Custom Scopes
Beyond the built-in SINGLETON and FOR_EVENT scopes, you can define custom named scopes with associated variables using defineInfactScope. Classes injected via @InjectFromScope are instantiated within that scope, and @InjectScopeVars lets them access the scope's variables.
Defining a Scope
Register a named scope with its variables before the app starts:
import { defineInfactScope } from 'moost'
defineInfactScope('tenant', {
tenantId: 'acme',
region: 'eu-west-1',
})@InjectFromScope
Marks a constructor parameter or property so that the injected class is instantiated within the named scope:
import { Injectable, InjectFromScope } from 'moost'
@Injectable()
class TenantConfig {
constructor(/* resolved within the 'tenant' scope */) {}
}
@Controller()
class BillingController {
constructor(@InjectFromScope('tenant') private config: TenantConfig) {}
}@InjectScopeVars
Resolves scope variables for classes living inside a custom scope. Use it inside classes that are instantiated via @InjectFromScope:
import { Injectable, InjectScopeVars } from 'moost'
@Injectable()
class TenantService {
constructor(
// inject a single variable by name
@InjectScopeVars('tenantId') private tenantId: string,
) {}
}Omit the name to receive the entire scope variables object:
@Injectable()
class TenantService {
constructor(
@InjectScopeVars() private vars: { tenantId: string; region: string },
) {}
}