In the services lesson you used inject(Service). Real apps also inject values: an API URL, feature flags, a logger. For that, you need tokens and providers.
What you will learn
- How to create and use an
InjectionToken - The provider types:
useValue,useClass,useFactory,useExisting - Where providers live and how scope affects instances
A configuration token
// src/app/di/app-config.token.ts
import { InjectionToken } from '@angular/core';
export interface AppSettings {
apiUrl: string;
featureBeta: boolean;
}
// Providing a factory in the token makes a sensible default available everywhere.
export const APP_SETTINGS = new InjectionToken<AppSettings>('APP_SETTINGS', {
providedIn: 'root',
factory: () => ({ apiUrl: 'http://localhost:3000', featureBeta: false }),
});
Read it with inject:
// src/app/di/api-client.ts
import { Injectable, inject } from '@angular/core';
import { APP_SETTINGS } from './app-config.token';
@Injectable({ providedIn: 'root' })
export class ApiClient {
private readonly settings = inject(APP_SETTINGS);
url(path: string): string {
return `${this.settings.apiUrl}/${path.replace(/^\//, '')}`;
}
}
In production, override the token in app.config.ts:
// src/app/app.config.ts
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient, withFetch } from '@angular/common/http';
import { routes } from './app.routes';
import { APP_SETTINGS } from './di/app-config.token';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(withFetch()),
{ provide: APP_SETTINGS, useValue: { apiUrl: 'https://api.example.com', featureBeta: true } },
],
};
The provider types
// src/app/di/logger.ts
import { InjectionToken } from '@angular/core';
export abstract class Logger {
abstract log(message: string): void;
}
export class ConsoleLogger extends Logger {
log(message: string): void {
console.log('[app]', message);
}
}
export class SilentLogger extends Logger {
log(): void {
// does nothing, useful in tests
}
}
export interface Plugin {
name: string;
}
export const PLUGINS = new InjectionToken<Plugin[]>('PLUGINS');
export const LOG_PREFIX = new InjectionToken<string>('LOG_PREFIX');
// src/app/di/provider-demo.ts
import { Component, inject } from '@angular/core';
import { APP_SETTINGS } from './app-config.token';
import { ConsoleLogger, LOG_PREFIX, Logger, PLUGINS, SilentLogger } from './logger';
@Component({
selector: 'app-provider-demo',
providers: [
// useClass: swap one implementation for another
{ provide: Logger, useClass: ConsoleLogger },
// useFactory: build a value with logic and other dependencies
{
provide: LOG_PREFIX,
useFactory: () => (inject(APP_SETTINGS).featureBeta ? 'BETA' : 'STABLE'),
},
// multi: collect many values under one token
{ provide: PLUGINS, useValue: { name: 'spellcheck' }, multi: true },
{ provide: PLUGINS, useValue: { name: 'autosave' }, multi: true },
],
template: `
<p>Prefix: {{ prefix }}</p>
<p>Plugins: {{ plugins.length }}</p>
<button type="button" (click)="logger.log('hello')">Log</button>
`,
})
export class ProviderDemo {
protected readonly logger = inject(Logger);
protected readonly prefix = inject(LOG_PREFIX);
protected readonly plugins = inject(PLUGINS);
}
export const TEST_PROVIDERS = [{ provide: Logger, useClass: SilentLogger }];
// src/app/app.ts
import { Component } from '@angular/core';
import { ProviderDemo } from './di/provider-demo';
@Component({
imports: [ProviderDemo],
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App {}
<!-- src/app/app.html -->
<main>
<h1>Providers</h1>
<app-provider-demo />
</main>
What each provider does
- `useValue` supplies a ready value, such as a settings object.
- `useClass` instantiates a class. Use it to replace an implementation, for example a real logger with a silent one in tests.
- `useFactory` runs a function, and can call
inject()to read other dependencies. - `useExisting` makes one token an alias for another.
- `multi: true` collects several providers under one token as an array.
An abstract class like Logger can itself be the token. Plain string tokens work, but InjectionToken is safer because it carries a type and a name.
Provider scope
Where you register a provider controls how many instances exist:
providedIn: 'root'orapp.config.ts: one instance for the whole app.providersin a component: a new instance for each component, shared with its children.- Route-level
providersin the route config: one instance for that part of the app.
Use component-level providers for state that should not be shared, such as the state of one form.
Replace providers in tests
TestBed.configureTestingModule({
providers: [{ provide: Logger, useClass: SilentLogger }],
});
This is the main reason to depend on tokens and abstract classes: you can swap the implementation without changing the code that uses it.
Try it yourself
- Add a
featureBetacheck to a component and flip it inapp.config.ts. - Create an
ApiClienttest that supplies a differentAPP_SETTINGSvalue. - Provide a service at route level and check that two routes get different instances.
Quick check
- Why use an
InjectionTokeninstead of a string? - What does
multi: truedo? - How can a component get its own instance of a service?
Next, intercept every HTTP request in one place with HTTP interceptors. See the DI guide.