title: 'Async providers · Nael Framework' description: 'Configure modules and tokens that depend on asynchronous work using useFactory, useClass, and useExisting patterns.'

Fundamentals

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.

DatabaseModule.forRootAsync
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.

BetterAuthModule.registerAsync(useClass)
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.

Sharing an options factory
@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.

Validating an async provider
@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.