Async providers
Not every dependency is ready the moment your module file executes. Async providers let Nael wait for Promises, wire dependencies first, and share the resolved value after bootstrapping—perfect for database pools, SDK clients, or secrets loaded from external services.
useFactory with injected deps
The most flexible form is useFactory. List whatever providers you need inside inject, perform async work, and return
either the provider value or the options object a module needs. Nael will await the Promise before exposing the dependency downstream.
import { Module } from '@nl-framework/core';import { ConfigModule, ConfigService } from '@nl-framework/config';import { DatabaseModule } from '@acme/database'; // imagine a feature module that exposes forRootAsync@Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), DatabaseModule.forRootAsync({ inject: [ConfigService], useFactory: async (config: ConfigService) => ({ url: config.get('database.url'), ssl: config.get('database.sslEnabled'), maxConnections: 10, }), }), ],})export class AppModule {}Keep factory functions side-effect free—no global state—and push configuration concerns into dedicated services to simplify testing.
useClass & factory interfaces
Many built-in modules (Config, BetterAuth, ORM, Scheduler) expose an OptionsFactory interface. Implement it when you prefer
an injectable class over an inline function. Nael instantiates the class once (respecting scope) and calls the factory method.
import { Injectable } from '@nl-framework/core';import type { BetterAuthModuleOptions, BetterAuthOptionsFactory } from '@nl-framework/auth';import { BetterAuthModule } from '@nl-framework/auth';import { ConfigService } from '@nl-framework/config';@Injectable()class AuthEnvService implements BetterAuthOptionsFactory { constructor(private readonly config: ConfigService) {} async createBetterAuthOptions(): Promise<BetterAuthModuleOptions> { return { projectId: this.config.get('auth.projectId'), secret: await this.config.getSecret('auth.secret'), }; }}@Module({ imports: [ BetterAuthModule.registerAsync({ useClass: AuthEnvService, imports: [ConfigModule], }), ],})export class AuthModule {}Classes can leverage constructor injection, caching, or memoization. They also compose nicely with testing modules that override the provider.
useExisting for re-use
If a module already provides the correct factory class, useExisting prevents duplicate instances. Nael will reference the
pre-registered token instead of creating a new class.
@Module({ providers: [AuthEnvService], exports: [AuthEnvService],})export class SharedConfigModule {}@Module({ imports: [ SharedConfigModule, BetterAuthModule.registerAsync({ useExisting: AuthEnvService, }), ],})export class AuthModule {}This pattern excels in monorepos where multiple features need the same config loader. Export the factory class once, then point to it via
useExisting everywhere else.
Bootstrapping order & lifecycle hooks
Async providers delay application bootstrap until they resolve. You can safely run sanity checks in onModuleInit or
onApplicationBootstrap knowing the dependencies are ready.
@Module({ imports: [CacheModule.forRootAsync({ useFactory: async () => ({ url: process.env.REDIS_URL! }) })],})export class AppModule implements OnModuleInit { constructor(private readonly cache: CacheService) {} async onModuleInit() { await this.cache.ping(); }}When a provider must refresh periodically (e.g., rotating secrets), create a scoped factory or schedule refresh logic after the app starts.