Providers
Providers are the backbone of MiiaJS's dependency injection (DI) system. Any class decorated with @Injectable() can be injected into other classes.
Defining a provider
import { Injectable } from '@miiajs/core'
@Injectable()
class UserService {
findAll() {
return [{ id: 1, name: 'Alice' }]
}
findById(id: string) {
return { id, name: 'Alice' }
}
}
Register the provider in a Module:
@Module({
controllers: [UserController],
providers: [UserService],
})
class UserModule {}
Injecting dependencies
Call inject() in a field initializer to resolve a dependency:
import { Controller, Get, inject } from '@miiajs/core'
@Controller('/users')
class UserController {
private userService = inject(UserService)
@Get('/')
list() {
return this.userService.findAll()
}
}
String tokens
You can register and resolve providers using string tokens instead of class references:
// Registration
container.register('API_KEY', () => process.env.API_KEY)
// Injection
class MyService {
private apiKey = inject<string>('API_KEY')
}
Scopes
Providers support three scopes:
| Scope | Behavior | Use case |
|---|---|---|
singleton (default) | One instance for the app lifetime | Services, config, repositories |
transient | New instance on every injection | Stateless utilities, factories |
request | New instance per HTTP request | Request-scoped state, loggers |
@Injectable({ scope: 'singleton' })
class DatabaseService {}
@Injectable({ scope: 'transient' })
class RequestParser {}
@Injectable({ scope: 'request' })
class RequestLogger {}
Request-scoped instances are automatically cleared after each HTTP request.
Optional injection
Use injectOptional() when a provider might not be registered:
import { injectOptional } from '@miiajs/core'
@Injectable()
class NotificationService {
private email = injectOptional(EmailService) // null if not registered
}
Runtime DI introspection (Resolver)
Resolver is a public read-only wrapper over the DI container. Inject it when the token is not known until runtime - plugin systems, conditional resolution, dynamic factories.
import { Injectable, inject, Resolver } from '@miiajs/core'
@Injectable()
class PluginRegistry {
private resolver = inject(Resolver)
load(name: string) {
const token = `plugin:${name}`
if (!this.resolver.has(token)) {
throw new Error(`unknown plugin: ${name}`)
}
return this.resolver.resolve(token)
}
}
The Resolver exposes only read operations. Mutations (register, destroyAll) stay internal to keep the public DI surface minimal:
| Method | Description |
|---|---|
has(token) | Returns true when a provider is registered for the token. |
resolve<T>(token) | Resolves the provider; throws if the token is not registered. |
resolveOptional<T>(token) | Resolves or returns null for an unregistered token. |
inject(Token) over resolver.resolve(Token) whenever you can. Field-init inject() is the idiomatic MiiaJS pattern - it composes with lifecycle hooks and keeps deps explicit at the class level. Reach for Resolver only when the token is dynamic or you need a has() check.Lifecycle hooks
Singleton providers can implement three lifecycle hooks:
@Injectable()
class DatabaseService {
async onInit() {
// Called during app.init() or lazily on first app.fetch(),
// after every singleton has been instantiated.
await this.connect()
}
async onReady() {
// Called after every onInit() has completed. Guaranteed to see
// a fully-initialized container regardless of registration order.
}
async onDestroy() {
// Called during app.destroy().
await this.disconnect()
}
}
| Hook | When called | Use case |
|---|---|---|
onInit() | During app.init() or lazily on first app.fetch(), after every singleton has been instantiated | Open connections, warm caches, load seed data |
onReady() | During app.init(), after every onInit() has completed | Ambient discovery, cross-wiring between providers |
onDestroy() | During app.destroy() | Close connections, flush buffers, release resources |
All three run on singleton providers only - transient and request scopes do not trigger lifecycle hooks.
The ordering gives onReady a guarantee onInit lacks: by the time any onReady() runs, every singleton in the container already has a fully-initialized instance, regardless of registration order. This is what makes the Discovery Service safe to use - it can scan every provider for decorator metadata at startup without worrying about which provider was registered first.
Factory providers
Register a provider with a custom factory function:
@Module({
providers: [
{
token: 'DATABASE',
factory: (container) => {
const url = container.resolve<string>('DATABASE_URL')
return createDatabase(url)
},
},
],
})
class AppModule {}
The Container
Each Miia instance owns its own Container. The container is accessible via app.get():
const app = new Miia().register(AppModule)
await app.init()
const userService = app.get(UserService)
const config = app.get<string>('API_KEY')