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 type | Collision risk | Discoverability | Best for |
|---|---|---|---|
string | High (typos, name clashes) | Easy to grep | Quick app-local config |
Symbol | None (unique identity) | Must import the symbol | Library and shared-kernel tokens |
class | None | Type-checked | Concrete 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 aNest can't resolve dependencieserror 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
EmailNotifierwould not make@Inject(NOTIFIER)resolvable in importing modules.
Best Practices
- Prefer
Symboltokens 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_CASEto 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.