Not everything you inject is a class. An API URL, a configuration, a set of feature flags, a function, a list of plugins — they all need a token. InjectionToken<T> is the typed key under which you provide and request them, and the provideX() functions are the modern way to expose a feature’s configuration (exactly like provideRouter() or provideHttpClient()).
InjectionToken
import { InjectionToken, inject } from '@angular/core';
// A typed token for a value that is NOT a class (config, function, string, object)
export const API_URL = new InjectionToken<string>('API_URL');
// With a default value: it works even if nobody provides it
export const PAGE_SIZE = new InjectionToken<number>('PAGE_SIZE', {
providedIn: 'root',
factory: () => 20,
});
// Usage
export class UsersApi {
private readonly baseUrl = inject(API_URL);
private readonly pageSize = inject(PAGE_SIZE);
}provideX() functions
// The provideX() pattern: the public API of a feature or a library
export interface FeatureFlags {
newCheckout: boolean;
darkPromo: boolean;
}
const DEFAULT_FLAGS: FeatureFlags = { newCheckout: false, darkPromo: false };
export const FEATURE_FLAGS = new InjectionToken<FeatureFlags>('FEATURE_FLAGS', {
providedIn: 'root',
factory: () => DEFAULT_FLAGS,
});
export function provideFeatureFlags(flags: Partial<FeatureFlags>): EnvironmentProviders {
return makeEnvironmentProviders([
{ provide: FEATURE_FLAGS, useValue: { ...DEFAULT_FLAGS, ...flags } },
]);
}{"newCheckout":false,"darkPromo":false,"betaBadge":true}{"newCheckout":true,"darkPromo":true,"betaBadge":true}// EnvironmentProviders (makeEnvironmentProviders) can be used ONLY at application
// or route level — not in a component’s `providers`. This is intentional: it is a “barrier”
// for global configuration (router, http, animations, libraries).
@Component({
// providers: [provideFeatureFlags(...)] ✗ error: EnvironmentProviders in a component
providers: [{ provide: FEATURE_FLAGS, useValue: { ...DEFAULT_FLAGS, newCheckout: true } }], // ✓
})
export class BetaZone {}multi: true — plugins
// Plugins: several providers for the same token, gathered into an array
export const EXPORTERS = new InjectionToken<Exporter[]>('EXPORTERS');
providers: [
{ provide: EXPORTERS, useValue: csvExporter, multi: true },
{ provide: EXPORTERS, useValue: jsonExporter, multi: true },
{ provide: EXPORTERS, useClass: PdfExporter, multi: true },
];
// The component doesn’t know how many formats exist — it receives all of them
readonly exporters = inject(EXPORTERS); // Exporter[]Choose a format.
Initializers
// Code that must run at startup, BEFORE the application is ready
export const appConfig: ApplicationConfig = {
providers: [
// replaces the old APP_INITIALIZER; can return a Promise / Observable
provideAppInitializer(() => inject(ConfigService).load()),
],
};
// At route or “environment” level (e.g. when the injector of a lazy feature is created)
{
path: 'admin',
providers: [provideEnvironmentInitializer(() => inject(AdminAnalytics).start())],
loadChildren: () => import('./admin/admin.routes'),
}Optional tokens
Sometimes a token really is optional, for example an analytics tracker that only some environments configure. inject(TOKEN, { optional: true }) returns null instead of throwing error NG0201. If a sensible default exists, put it in the factory instead, so you do not have to check for null everywhere.
export const ANALYTICS = new InjectionToken<Analytics>('ANALYTICS');
export class Checkout {
private readonly analytics = inject(ANALYTICS, { optional: true }); // null if nobody provided it
pay() {
this.analytics?.track('pay');
}
}Lightweight tokens in libraries
If a library component looks for a child with contentChild(LibHeader), the LibHeader class ends up in the bundle even in apps that never use it. The solution from the Angular docs: look up a small abstract class, and the real component provides itself under that token. If nobody uses the component, the bundler removes it.
// the token: an empty abstract class, almost no code in the bundle
export abstract class LibCardHeader {}
@Component({
selector: 'lib-card-header',
providers: [{ provide: LibCardHeader, useExisting: CardHeader }], // the real component provides itself under the token
template: '<ng-content />',
})
export class CardHeader extends LibCardHeader {}
@Component({ selector: 'lib-card', template: '...' })
export class Card {
// look up by token, not by CardHeader: if nobody uses CardHeader, it leaves the bundle
protected readonly header = contentChild(LibCardHeader);
}Tokens in tests
You replace a token in tests exactly like a service: provide it in TestBed with another value. That way you test the same component with different flags or another API URL without touching the code.
it('shows the new checkout when the flag is on', () => {
TestBed.configureTestingModule({
providers: [
{ provide: FEATURE_FLAGS, useValue: { newCheckout: true } },
{ provide: API_URL, useValue: 'http://localhost/test-api' },
],
});
const fixture = TestBed.createComponent(CheckoutPage);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('New checkout');
});What to choose
Class / @Service() → you have logic (methods), dependencies, state
InjectionToken<T> → values: config, URLs, functions, constants, interfaces
InjectionToken + multi → plugins / extensions (validators, exporters, interceptors)
provideX() → the public configuration API for a feature / libraryThe hierarchy in which Angular looks up a token is covered in Providers & the injector hierarchy; tokens used as ports between layers, in Hexagonal architecture (ports & adapters).
Common pitfalls
- String tokens (
@Inject('API_URL')) — no types, possible collisions. Always useInjectionToken. - Forgetting
multi: trueon a provider → it overwrites the whole list instead of adding to it. - An interface as a token:
inject(MyInterface)doesn’t work — interfaces don’t exist at runtime. Use a token or an abstract class. - Slow initializers delay the application start. Load only what is strictly needed before the first render.
Cheat sheet
new InjectionToken<T>('NAME') // someone must provide it
new InjectionToken<T>('NAME', { providedIn: 'root', factory }) // with a default value
{ provide: TOKEN, useValue: value }
{ provide: TOKEN, useFactory: () => create(inject(Other)) }
{ provide: TOKEN, useExisting: OtherToken }
{ provide: TOKEN, useClass: Impl, multi: true } // collected into an array
inject(TOKEN) // NG0201 if missing
inject(TOKEN, { optional: true }) // null if missing
makeEnvironmentProviders([...]) // only in app.config / routes
provideAppInitializer(() => inject(Config).load()) // runs before the first render