Custom providers
Everything in Nael is just a provider resolved by a token. By shaping those tokens yourself—value objects, alternate classes, or factories—you can plug in third-party SDKs, swap implementations per environment, or stub dependencies in tests without rewriting consumers.
Tokens and value providers
A provider is either a class marked with @Injectable() or an object literal following the Provider union
(ClassProvider, ValueProvider, or FactoryProvider). Value providers shine when you already have
an instance—like a database client or preconfigured SDK—and want Nael’s container to share it everywhere.
import { Inject, Injectable, Module } from '@nl-framework/core';type CacheClient = { get(key: string): Promise<string | undefined>; set(key: string, value: string): Promise<void>;};export const CACHE_TOKEN = Symbol('CACHE_CLIENT');@Injectable()export class CacheService { constructor(@Inject(CACHE_TOKEN) private readonly cache: CacheClient) {} async getOrSet(key: string, compute: () => Promise<string>) { const cached = await this.cache.get(key); if (cached) { return cached; } const value = await compute(); await this.cache.set(key, value); return value; }}@Module({ providers: [ { provide: CACHE_TOKEN, useValue: createCacheClient(), // imagine this establishes a Redis connection }, CacheService, ], exports: [CACHE_TOKEN, CacheService],})export class CacheModule {}Prefer Symbol tokens for cross-package safety, then inject them with @Inject(). Exporting both the token and the
consumer class lets downstream modules re-use the same instance without knowing how it is created.
Alias to another class
Sometimes you care about the abstraction (an interface or abstract class) rather than a concrete implementation. useClass
lets you bind a token to a class at registration time, so the consumer keeps the same constructor signature while the framework decides which
implementation to instantiate.
import { Injectable, Module } from '@nl-framework/core';abstract class MailClient { abstract send(to: string, subject: string, body: string): Promise<void>;}@Injectable()class SendgridMailer implements MailClient { async send(to: string, subject: string, body: string) { /* call vendor API */ }}@Injectable()class LocalMailer implements MailClient { async send(to: string, subject: string, body: string) { console.log('mail', { to, subject, body }); }}const mailerProvider = { provide: MailClient, useClass: process.env.NODE_ENV === 'development' ? LocalMailer : SendgridMailer,};@Module({ providers: [mailerProvider], exports: [MailClient],})export class NotificationsModule {}This pattern is perfect for swapping real infrastructure with fakes in development or testing. You can also redeclare the provider inside a testing module to override it temporarily.
Factory providers & dependencies
Factories run after the container resolves anything listed in inject. They may return values synchronously or asynchronously,
which makes them ideal for bootstrapping SDKs that need configuration, logging, or other providers. Nael will await the Promise before exposing
the resulting instance to the rest of the application.
import { Module } from '@nl-framework/core';const databaseProvider = { provide: 'DB_CONNECTION', useFactory: async (config: ConfigService, logger: Logger) => { const options = config.get('database'); const connection = await createConnection(options); logger.log('[db] connected'); return connection; }, inject: [ConfigService, Logger],};@Module({ providers: [databaseProvider], exports: ['DB_CONNECTION'],})export class DatabaseModule {}You can combine factories with module exports to share expensive singletons (database pools, caches) without leaking implementation details. For advanced cases, factories can even emit different values per scope by inspecting request-specific data.
Handling circular dependencies
When two providers depend on each other, wrap the token in forwardRef(() => Token). The container postpones evaluation until both
classes are defined, avoiding the dreaded undefined dependency issues.
import { Inject, Injectable, Module, forwardRef } from '@nl-framework/core';@Injectable()export class AccountsService { constructor(private readonly payments: PaymentsService) {}}@Injectable()export class PaymentsService { constructor( @Inject(forwardRef(() => AccountsService)) private readonly accounts: AccountsService, ) {}}@Module({ providers: [AccountsService, PaymentsService],})export class BillingModule {}Use forward references sparingly—they are a smell that the collaboration might be extracted into a third provider—but they unblock legitimate scenarios such as domain services reacting to each other’s events.