Angular Injection Tokens and Providers: Configuration and Testing

Angular Tutorial: Beginner to Advanced · 15. Architecture, configuration and errors · 4 min read · Updated

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' or app.config.ts: one instance for the whole app.
  • providers in a component: a new instance for each component, shared with its children.
  • Route-level providers in 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

  1. Add a featureBeta check to a component and flip it in app.config.ts.
  2. Create an ApiClient test that supplies a different APP_SETTINGS value.
  3. Provide a service at route level and check that two routes get different instances.

Quick check

  1. Why use an InjectionToken instead of a string?
  2. What does multi: true do?
  3. 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.

Frequently asked questions

Is providedIn root always best?

It is the right default for shared services, and it allows tree shaking. Use narrower providers only when state must not be shared.

Can I inject things outside classes?

Yes, in any injection context: constructors, field initializers, factories, guards and interceptors. Calling inject() elsewhere throws an error.