Angular Dependency Injection
Master Angular's dependency injection system for building modular, testable applications with proper service architecture.
DI Fundamentals
Angular's DI is hierarchical and uses decorators and providers:
import { Injectable } from '@angular/core';
// Service injectable at root level @Injectable({ providedIn: 'root' }) export class UserService { private users: User[] = [];
getUsers(): User[] { return this.users; }
addUser(user: User): void { this.users.push(user); } }
// Component injection import { Component } from '@angular/core';
@Component({
selector: 'app-user-list',
template: <div *ngFor="let user of users"> {{ user.name }} </div>
})
export class UserListComponent {
users: User[];
constructor(private userService: UserService) { this.users = this.userService.getUsers(); } }
Provider Types
useClass - Class Provider
import { Injectable, Provider } from '@angular/core';
// Interface interface Logger { log(message: string): void; }
// Implementations @Injectable() export class ConsoleLogger implements Logger { log(message: string): void { console.log(message); } }
@Injectable() export class FileLogger implements Logger { log(message: string): void { // Write to file } }
// Provider configuration const loggerProvider: Provider = { provide: Logger, useClass: ConsoleLogger // or FileLogger based on env };
// In module @NgModule({ providers: [loggerProvider] }) export class AppModule {}
// Usage export class MyComponent { constructor(private logger: Logger) { this.logger.log('Component initialized'); } }
useValue - Value Provider
import { InjectionToken } from '@angular/core';
// Configuration object export interface AppConfig { apiUrl: string; timeout: number; retries: number; }
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
// Provider const configProvider: Provider = { provide: APP_CONFIG, useValue: { apiUrl: 'https://api.example.com', timeout: 5000, retries: 3 } };
// Module @NgModule({ providers: [configProvider] }) export class AppModule {}
// Usage export class ApiService { constructor(@Inject(APP_CONFIG) private config: AppConfig) { console.log(this.config.apiUrl); } }
useFactory - Factory Provider
import { Injectable, InjectionToken } from '@angular/core';
export const API_URL = new InjectionToken<string>('api.url');
// Factory function export function apiUrlFactory(config: AppConfig): string { return config.production ? 'https://api.prod.example.com' : 'https://api.dev.example.com'; }
// Provider const apiUrlProvider: Provider = { provide: API_URL, useFactory: apiUrlFactory, deps: [AppConfig] // Dependencies for factory };
// Complex factory with multiple deps export function httpClientFactory( handler: HttpHandler, config: AppConfig, logger: Logger ): HttpClient { logger.log('Creating HTTP client'); return new HttpClient(handler); }
const httpClientProvider: Provider = { provide: HttpClient, useFactory: httpClientFactory, deps: [HttpHandler, AppConfig, Logger] };
useExisting - Alias Provider
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' }) export class NewLogger { log(message: string): void { console.log('[NEW]', message); } }
// Alias old logger to new logger const oldLoggerProvider: Provider = { provide: 'OldLogger', useExisting: NewLogger };
// Usage export class MyComponent { constructor( @Inject('OldLogger') private logger: NewLogger ) { this.logger.log('Using aliased logger'); } }
Injection Tokens
InjectionToken - Type-Safe Tokens
import { InjectionToken } from '@angular/core';
// Primitive token export const MAX_RETRIES = new InjectionToken<number>('max.retries', { providedIn: 'root', factory: () => 3 // Default value });
// Object token export interface FeatureFlags { enableNewUI: boolean; enableBeta: boolean; }
export const FEATURE_FLAGS = new InjectionToken<FeatureFlags>( 'feature.flags', { providedIn: 'root', factory: () => ({ enableNewUI: false, enableBeta: false }) } );
// Usage @Injectable() export class ApiService { constructor( @Inject(MAX_RETRIES) private maxRetries: number, @Inject(FEATURE_FLAGS) private flags: FeatureFlags ) {} }
String Tokens (Legacy)
// Not type-safe, avoid when possible const providers: Provider[] = [ { provide: 'API_URL', useValue: 'https://api.example.com' }, { provide: 'TIMEOUT', useValue: 5000 } ];
// Usage export class MyService { constructor( @Inject('API_URL') private apiUrl: string, @Inject('TIMEOUT') private timeout: number ) {} }
Hierarchical Injectors
Root Injector
// Singleton across entire app @Injectable({ providedIn: 'root' }) export class GlobalService { private state = {}; }
// Same instance everywhere
Module Injector
@Injectable() export class ModuleService { // Service specific to module }
@NgModule({ providers: [ModuleService] }) export class FeatureModule {}
// Different instance per module
Component Injector
@Injectable() export class ComponentService { private data = []; }
@Component({ selector: 'app-my-component', template: '...', providers: [ComponentService] // New instance per component }) export class MyComponent { constructor(private service: ComponentService) {} }
// Each component instance gets its own service instance
Element Injector
@Directive({ selector: '[appHighlight]', providers: [DirectiveService] }) export class HighlightDirective { constructor(private service: DirectiveService) {} }
// Each directive instance gets its own service
ProvidedIn Options
// Root - singleton @Injectable({ providedIn: 'root' }) export class RootService {}
// Platform - shared across multiple apps @Injectable({ providedIn: 'platform' }) export class PlatformService {}
// Any - new instance per module @Injectable({ providedIn: 'any' }) export class AnyService {}
// Module - specific module @Injectable({ providedIn: FeatureModule }) export class FeatureService {}
Multi-Providers
import { InjectionToken } from '@angular/core';
// Token for multiple providers export const HTTP_INTERCEPTORS = new InjectionToken<HttpInterceptor[]>('http.interceptors');
// Multiple implementations @Injectable() export class AuthInterceptor implements HttpInterceptor { intercept(req: HttpRequest<any>, next: HttpHandler) { // Add auth header return next.handle(req); } }
@Injectable() export class LoggingInterceptor implements HttpInterceptor { intercept(req: HttpRequest<any>, next: HttpHandler) { // Log request return next.handle(req); } }
// Register as multi-providers const providers: Provider[] = [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true } ];
// Inject as array export class HttpService { constructor( @Inject(HTTP_INTERCEPTORS) private interceptors: HttpInterceptor[] ) { // interceptors is array of all registered interceptors } }
Optional and Self Decorators
@Optional - Allow Missing Dependencies
import { Optional } from '@angular/core';
@Injectable() export class MyService { constructor( @Optional() private logger?: Logger ) { // logger might be undefined this.logger?.log('Service created'); } }
@Self - Only Current Injector
import { Self } from '@angular/core';
@Component({ selector: 'app-my-component', providers: [LocalService] }) export class MyComponent { constructor( @Self() private local: LocalService // Only from this component ) {} }
@SkipSelf - Skip Current Injector
import { SkipSelf } from '@angular/core';
@Component({ selector: 'app-child', providers: [SharedService] }) export class ChildComponent { constructor( @SkipSelf() private parent: SharedService // From parent, not self ) {} }
@Host - Host Element Injector
import { Host } from '@angular/core';
@Directive({ selector: '[appChild]' }) export class ChildDirective { constructor( @Host() private parent: ParentComponent // From host component ) {} }
ForRoot and ForChild Patterns
import { NgModule, ModuleWithProviders } from '@angular/core';
@NgModule({}) export class SharedModule { // For root module - configures services static forRoot(config: SharedConfig): ModuleWithProviders<SharedModule> { return { ngModule: SharedModule, providers: [ SharedService, { provide: SHARED_CONFIG, useValue: config } ] }; }
// For feature modules - no service providers static forChild(): ModuleWithProviders<SharedModule> { return { ngModule: SharedModule, providers: [] // No providers, use root services }; } }
// Usage in AppModule @NgModule({ imports: [ SharedModule.forRoot({ apiUrl: 'https://api.example.com' }) ] }) export class AppModule {}
// Usage in feature module @NgModule({ imports: [ SharedModule.forChild() ] }) export class FeatureModule {}
Tree-Shakable Providers
// Traditional (not tree-shakable) @Injectable() export class OldService {}
@NgModule({ providers: [OldService] }) export class AppModule {}
// Tree-shakable (preferred) @Injectable({ providedIn: 'root' }) export class NewService {}
// No need to register in module // Service is removed if never injected
Testing with DI
TestBed Provider Overrides
import { TestBed } from '@angular/core/testing';
describe('MyComponent', () => { let mockUserService: jasmine.SpyObj<UserService>;
beforeEach(() => { mockUserService = jasmine.createSpyObj('UserService', ['getUsers']);
TestBed.configureTestingModule({
declarations: [MyComponent],
providers: [
{ provide: UserService, useValue: mockUserService }
]
});
});
it('should get users', () => { mockUserService.getUsers.and.returnValue([]); const fixture = TestBed.createComponent(MyComponent); // Test component with mock }); });
Spy on Dependencies
import { TestBed } from '@angular/core/testing';
describe('UserService', () => { let service: UserService; let httpMock: jasmine.SpyObj<HttpClient>;
beforeEach(() => { httpMock = jasmine.createSpyObj('HttpClient', ['get', 'post']);
TestBed.configureTestingModule({
providers: [
UserService,
{ provide: HttpClient, useValue: httpMock }
]
});
service = TestBed.inject(UserService);
});
it('should fetch users', () => { httpMock.get.and.returnValue(of([])); service.getUsers().subscribe(); expect(httpMock.get).toHaveBeenCalled(); }); });
When to Use This Skill
Use angular-dependency-injection when building modern, production-ready applications that require:
-
Modular service architecture
-
Testable components and services
-
Configuration management
-
Plugin/extension systems
-
Multi-provider patterns (interceptors, validators)
-
Complex service hierarchies
-
Lazy-loaded module isolation
-
Tree-shakable code
Angular DI Best Practices
-
Use providedIn: 'root'
-
Tree-shakable and singleton
-
Use InjectionToken - Type-safe over string tokens
-
Favor composition - Inject small, focused services
-
Use factories for complex creation - useFactory for dynamic values
-
Test with mocks - Override providers in TestBed
-
Follow forRoot/forChild pattern - For shared modules
-
Use @Optional sparingly - Prefer defaults or required deps
-
Document multi-providers - Clear contract for extensions
-
Avoid circular dependencies - Refactor to common service
-
Use providedIn module - For module-specific services
DI Pitfalls and Gotchas
-
Circular dependencies - A depends on B, B depends on A
-
Providing in component - Creates new instance per component
-
Missing providers - Runtime error if not provided
-
Wrong injector level - Service not found in hierarchy
-
Forgetting multi: true - Overrides instead of adding
-
String token collisions - Use InjectionToken instead
-
Not using forRoot - Multiple service instances
-
Providing eagerly - Use providedIn for tree-shaking
-
Testing without mocks - Real dependencies in tests
-
Complex factory deps - Hard to test and maintain
Advanced DI Patterns
Service with Configuration
import { Injectable, Inject, InjectionToken } from '@angular/core';
export interface ApiConfig { baseUrl: string; timeout: number; }
export const API_CONFIG = new InjectionToken<ApiConfig>('api.config');
@Injectable({ providedIn: 'root' }) export class ApiService { constructor(@Inject(API_CONFIG) private config: ApiConfig) {}
get(endpoint: string) {
return fetch(${this.config.baseUrl}/${endpoint}, {
signal: AbortSignal.timeout(this.config.timeout)
});
}
}
// Module @NgModule({ providers: [ { provide: API_CONFIG, useValue: { baseUrl: 'https://api.example.com', timeout: 5000 } } ] }) export class AppModule {}
Abstract Class Provider
import { Injectable } from '@angular/core';
// Abstract class export abstract class DataService<T> { abstract get(id: string): Observable<T>; abstract save(item: T): Observable<T>; }
// Implementation @Injectable() export class UserDataService implements DataService<User> { get(id: string): Observable<User> { // Implementation }
save(user: User): Observable<User> { // Implementation } }
// Provider @NgModule({ providers: [ { provide: DataService, useClass: UserDataService } ] }) export class FeatureModule {}
// Usage export class MyComponent { constructor(private dataService: DataService<User>) {} }
Conditional Provider
import { Injectable, InjectionToken } from '@angular/core'; import { environment } from './environments/environment';
@Injectable() export class DevLogger { log(message: string) { console.log('[DEV]', message); } }
@Injectable() export class ProdLogger { log(message: string) { // Send to logging service } }
// Factory chooses implementation export function loggerFactory(): Logger { return environment.production ? new ProdLogger() : new DevLogger(); }
const loggerProvider: Provider = { provide: Logger, useFactory: loggerFactory };
Scope Isolation
// Parent service @Injectable({ providedIn: 'root' }) export class GlobalState { count = 0; }
// Child service (isolated) @Injectable() export class LocalState { count = 0; // Independent per component }
@Component({ selector: 'app-counter', providers: [LocalState] // New instance per component }) export class CounterComponent { constructor( public global: GlobalState, public local: LocalState ) {}
incrementGlobal() { this.global.count++; // Affects all components }
incrementLocal() { this.local.count++; // Only this component } }
Resources
-
Angular Dependency Injection Guide
-
Hierarchical Injectors
-
DI in Action
-
Injectable Services
-
Providers
-
Testing with DI