Skip to content
NestJS ns providers 4 min read

Injection Tokens

NestJS resolves dependencies by looking them up in its DI container using a token. When you inject a class, the class itself is the token, so everything is implicit and clean. But the moment you want to inject a value that has no class, an interface, or multiple implementations of the same contract, you need to name that dependency explicitly. Injection tokens are how you do that, and @Inject is how you ask for them.

How tokens work in the DI container

Every provider you register lives in the container under a key. For a standard @Injectable() class, that key is the class reference. Nest sees the constructor parameter type, finds the matching provider, and injects it. This is why you rarely think about tokens at all.

@Injectable()
export class UsersService {
  constructor(private readonly repo: UserRepository) {} // token = UserRepository
}

The problem appears when the thing you want to inject is not a class: a configuration object, a connection string, a third-party client instance, or a value chosen by an interface rather than a concrete type. TypeScript interfaces and primitive values do not exist at runtime, so Nest has nothing to use as a key. You must supply one yourself.

Defining a custom token

A token can be a string or a Symbol. You attach it to a provider through the provide property and consume it with the @Inject() decorator.

// app.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';

@Module({
  providers: [
    {
      provide: 'API_BASE_URL',
      useValue: 'https://api.devcraftly.com/v1',
    },
    UsersService,
  ],
})
export class AppModule {}
// users.service.ts
import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class UsersService {
  constructor(@Inject('API_BASE_URL') private readonly baseUrl: string) {}

  endpoint(path: string): string {
    return `${this.baseUrl}/${path}`;
  }
}

Calling usersService.endpoint('users') returns:

Output:

https://api.devcraftly.com/v1/users

String vs Symbol tokens

String tokens are simple but globally namespaced — two libraries that both register 'CONFIG' will collide silently. Symbols are unique by identity, which removes that risk, at the cost of having to export and import the symbol everywhere it is used.

Token typeCollision riskDiscoverabilityBest for
stringHigh (typos, name clashes)Easy to grepQuick app-local config
SymbolNone (unique identity)Must import the symbolLibrary and shared-kernel tokens
classNoneType-checkedConcrete class providers

A common pattern is to centralize tokens in a constants file so they are typed and reusable:

// tokens.ts
export const MAILER = Symbol('MAILER');
export const PAYMENT_CONFIG = 'PAYMENT_CONFIG';

Always import a shared token from a single source. Re-declaring Symbol('MAILER') in another file creates a different symbol, and Nest will throw a Nest can't resolve dependencies error at startup.

Injecting interfaces

Because interfaces vanish at compile time, you cannot inject them by type. Instead, define a token, register the implementation against it, and inject the token. This keeps your service depending on the abstraction while DI supplies a concrete class.

// notifier.interface.ts
export interface Notifier {
  send(to: string, message: string): Promise<void>;
}

export const NOTIFIER = Symbol('NOTIFIER');
// email-notifier.ts
import { Injectable } from '@nestjs/common';
import { Notifier } from './notifier.interface';

@Injectable()
export class EmailNotifier implements Notifier {
  async send(to: string, message: string): Promise<void> {
    console.log(`Email to ${to}: ${message}`);
  }
}
// app.module.ts
import { Module } from '@nestjs/common';
import { NOTIFIER } from './notifier.interface';
import { EmailNotifier } from './email-notifier';
import { AlertsService } from './alerts.service';

@Module({
  providers: [
    { provide: NOTIFIER, useClass: EmailNotifier },
    AlertsService,
  ],
})
export class AppModule {}
// alerts.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { Notifier, NOTIFIER } from './notifier.interface';

@Injectable()
export class AlertsService {
  constructor(@Inject(NOTIFIER) private readonly notifier: Notifier) {}

  async raise(message: string): Promise<void> {
    await this.notifier.send('[email protected]', message);
  }
}

Swapping EmailNotifier for an SmsNotifier is now a one-line change in the module — the consuming service never changes.

Exporting tokens across modules

A token-based provider is only injectable in modules that can see it. Add the token to the module’s exports array, then import that module elsewhere.

@Module({
  providers: [{ provide: NOTIFIER, useClass: EmailNotifier }],
  exports: [NOTIFIER],
})
export class NotificationsModule {}

The value you export is the token, not the implementation class. Exporting EmailNotifier would not make @Inject(NOTIFIER) resolvable in importing modules.

Best Practices

  • Prefer Symbol tokens for anything shared across modules or published in a library to avoid name collisions.
  • Centralize tokens in a dedicated constants file and import them; never re-declare a symbol.
  • Pair an injection token with its TypeScript interface in the same file so the contract and key stay together.
  • Name string tokens in SCREAMING_SNAKE_CASE to signal they are container keys, not ordinary values.
  • Use tokens to depend on abstractions, letting you swap implementations (useClass, useValue, useFactory) without touching consumers.
  • Always export the token from the providing module when it must be injected elsewhere.
Last updated June 14, 2026
Was this helpful?