Dependency Injection Providers — useClass, useValue, useFactory, useExisting

📋 Table of Contents
  1. Provider Configurations
  2. Common Mistakes

Angular’s DI system supports multiple provider configurations beyond simply registering a class. useClass swaps one implementation for another (production service vs mock). useValue injects primitive values and objects. useFactory creates services with runtime logic. InjectionToken provides type-safe tokens for non-class dependencies. Together these allow environment-specific configuration, A/B testing, and testable architecture without changing component or service code.

Provider Configurations

import { InjectionToken, inject } from '@angular/core';
import { environment } from '@env/environment';

// ── InjectionToken — for non-class dependencies ───────────────────────────
export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL');
export const APP_CONFIG   = new InjectionToken<AppConfig>('APP_CONFIG');

export interface AppConfig {
  maxPageSize: number;
  defaultPageSize: number;
  featureFlags: { signalR: boolean; darkMode: boolean };
}

// ── app.config.ts — configure providers ───────────────────────────────────
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(withInterceptors([authInterceptor])),

    // ── useValue — inject a constant ──────────────────────────────────────
    { provide: API_BASE_URL, useValue: environment.apiBaseUrl },

    // ── useValue — inject a config object ─────────────────────────────────
    {
      provide:  APP_CONFIG,
      useValue: { maxPageSize: 100, defaultPageSize: 10,
                  featureFlags: { signalR: true, darkMode: false } } as AppConfig,
    },

    // ── useClass — swap implementation (environment-based) ─────────────────
    environment.production
      ? { provide: PostsService, useClass: ProductionPostsService }
      : { provide: PostsService, useClass: MockPostsService },

    // ── useFactory — service needing runtime construction ─────────────────
    {
      provide:    LoggingService,
      useFactory: () => {
        const config = inject(APP_CONFIG);
        return new LoggingService(config.featureFlags.signalR ? 'verbose' : 'warn');
      },
    },

    // ── useExisting — alias one token to another ──────────────────────────
    // Both AuthService and IAuthService token resolve to the same instance
    { provide: IAuthService, useExisting: AuthService },
  ],
};

// ── Injecting tokens in services/components ───────────────────────────────
@Injectable({ providedIn: 'root' })
export class PostsService {
  private baseUrl = inject(API_BASE_URL);       // inject the token
  private config  = inject(APP_CONFIG);         // inject the config object
  private http    = inject(HttpClient);

  getPublished(page = 1) {
    const size = this.config.defaultPageSize;
    return this.http.get<PagedResult<PostSummaryDto>>(
      `${this.baseUrl}/api/posts?page=${page}&size=${size}`
    );
  }
}

// ── Route-level providers — scoped to a route subtree ─────────────────────
// app.routes.ts
export const routes: Routes = [
  {
    path:      'admin',
    providers: [
      // AdminService only exists in the admin subtree
      { provide: AdminService, useClass: AdminService },
    ],
    loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent),
  },
];
Note: InjectionToken is the correct way to provide non-class values (strings, numbers, objects, interfaces) to Angular’s DI system. A plain string token (provide: 'apiUrl') works but is error-prone — a typo in the string name silently fails to inject the value. InjectionToken is type-safe and IDE-friendly: const API_URL = new InjectionToken<string>('API_URL') gives you a typed, referenceable token. Use InjectionToken for all non-class provider keys.
Tip: Route-level providers scope services to a route subtree — the service exists only while the user is on that route and is destroyed when they navigate away. This is ideal for feature-specific services that should not persist globally (an admin service, a checkout flow service, a wizard state service). Use route providers for services whose state should be fresh on every entry to the route and discarded on exit.
Warning: useClass always creates a new instance of the specified class — it does not reuse an existing instance. If MockPostsService is already provided elsewhere, { provide: PostsService, useClass: MockPostsService } creates a separate instance. To reuse an existing instance, use useExisting instead: { provide: PostsService, useExisting: MockPostsService }. The difference matters when the class has stateful signals or cached data that should be shared.

Common Mistakes

Mistake 1 — Using string literals as provider tokens (typo-prone, no type safety)

❌ Wrong — { provide: 'apiUrl', useValue: environment.apiBaseUrl }; string mismatch is silently null.

✅ Correct — export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL'); typed and refactorable.

Mistake 2 — useClass when you need useExisting (creates duplicate instance)

❌ Wrong — { provide: ILogger, useClass: ConsoleLogger } creates a second ConsoleLogger instance.

✅ Correct — { provide: ILogger, useExisting: ConsoleLogger } aliases to the already-registered instance.

🧠 Test Yourself

A LoggingService needs to read a feature flag from AppConfig at construction time. Which provider type is appropriate?