Providers

Injectable services, repositories, and the dependency injection system.

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:

ScopeBehaviorUse case
singleton (default)One instance for the app lifetimeServices, config, repositories
transientNew instance on every injectionStateless utilities, factories
requestNew instance per HTTP requestRequest-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:

MethodDescription
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.
Prefer 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()
  }
}
HookWhen calledUse case
onInit()During app.init() or lazily on first app.fetch(), after every singleton has been instantiatedOpen connections, warm caches, load seed data
onReady()During app.init(), after every onInit() has completedAmbient 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')