Overview
Master Angular's dependency injection system for building modular, testable applications with proper service architecture.
DI Fundamentals
Angular's DI uses the inject() function (Angular 14+) as the preferred
injection mechanism â no constructor parameters needed:
typescript
import { Injectable, inject } 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);
}
}
// Standalone component injection via inject()
import { Component } from '@angular/core';
@Component({
selector: 'app-user-list',
standalone: true,
template: `
@for (user of users; track user.id) {
<div>{{ user.name }}</div>
}
`
})
export class UserListComponent {
private readonly userService = inject(UserService);
users = this.userService.getUsers();
}
Provider Types
useClass - Class Provider
typescript
import { Injectable, Provider, Component, inject } 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
};
// Standalone component â providers go here, not in NgModule
@Component({
selector: 'app-my-component',
standalone: true,
providers: [loggerProvider],
template: `...`
})
export class MyComponent {
private readonly logger = inject(Logger);
constructor() {
this.logger.log('Component initialized');
}
}
useValue - Value Provider
typescript
import { InjectionToken, Component, inject } 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 = {
provide: APP_CONFIG,
useValue: {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
}
};
// bootstrapApplication (standalone)
bootstrapApplication(AppComponent, {
providers: [configProvider]
});
// Usage via inject()
export class ApiService {
private readonly config = inject(APP_CONFIG);
constructor() {
console.log(this.config.apiUrl);
}
}
useFactory - Factory Provider
typescript
import { Injectable, InjectionToken, inject } from '@angular/core';
export const API_URL = new InjectionToken<string>('api.url');
// Use inject() inside the factory â no deps array needed
const apiUrlProvider = {
provide: API_URL,
useFactory: () => {
const config = inject(AppConfig);
return config.production
? 'https://api.prod.example.com'
: 'https://api.dev.example.com';
}
};
// Complex factory â all deps resolved via inject()
const httpClientProvider = {
provide: HttpClient,
useFactory: () => {
const handler = inject(HttpHandler);
const logger = inject(Logger);
logger.log('Creating HTTP client');
return new HttpClient(handler);
}
};
useExisting - Alias Provider
typescript
import { Injectable, inject } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class NewLogger {
log(message: string): void {
console.log('[NEW]', message);
}
}
// Alias an abstract token to the concrete implementation
export abstract class Logger {
abstract log(message: string): void;
}
const loggerAlias: Provider = {
provide: Logger,
useExisting: NewLogger
};
// Usage
export class MyComponent {
private readonly logger = inject(Logger);
constructor() {
this.logger.log('Using aliased logger');
}
}
Injection Tokens
InjectionToken - Type-Safe Tokens
typescript
import { InjectionToken, inject } 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 via inject()
@Injectable()
export class ApiService {
private readonly maxRetries = inject(MAX_RETRIES);
private readonly flags = inject(FEATURE_FLAGS);
}
Hierarchical Injectors
Root Injector
typescript
// Singleton across entire app
@Injectable({
providedIn: 'root'
})
export class GlobalService {
private state = {};
}
Component Injector
typescript
@Injectable()
export class ComponentService {
private data = [];
}
@Component({
selector: 'app-my-component',
standalone: true,
template: '...',
providers: [ComponentService] // New instance per component
})
export class MyComponent {
private readonly service = inject(ComponentService);
}
// Each component instance gets its own service instance
Element Injector
typescript
@Directive({
selector: '[appHighlight]',
standalone: true,
providers: [DirectiveService]
})
export class HighlightDirective {
private readonly service = inject(DirectiveService);
}
ProvidedIn Options
typescript
// Root - singleton across entire app
@Injectable({
providedIn: 'root'
})
export class RootService {}
// Platform - shared across multiple apps
@Injectable({
providedIn: 'platform'
})
export class PlatformService {}
Optional and Scoped Injection
Use inject() options instead of @Optional, @Self, @SkipSelf, @Host:
typescript
import { inject } from '@angular/core';
export class MyService {
// @Optional() equivalent
private readonly logger = inject(Logger, { optional: true });
// @Self() equivalent â only from this component's injector
private readonly local = inject(LocalService, { self: true });
// @SkipSelf() equivalent â skip this injector, look up the tree
private readonly parent = inject(SharedService, { skipSelf: true });
// @Host() equivalent â stop at host element
private readonly host = inject(ParentComponent, { host: true });
// Combine options
readonly #dep = inject(Dep, {
optional: true,
self: true,
});
constructor() {
this.logger?.log('Service created');
}
}
Environment Providers (replaces ForRoot/ForChild)
Use makeEnvironmentProviders and provideX() functions instead of
NgModule.forRoot() / NgModule.forChild():
typescript
import { makeEnvironmentProviders, EnvironmentProviders } from '@angular/core';
export interface SharedConfig {
apiUrl: string;
}
export const SHARED_CONFIG = new InjectionToken<SharedConfig>('shared.config');
// Replace forRoot() with a provideX() function
export function provideShared(config: SharedConfig): EnvironmentProviders {
return makeEnvironmentProviders([
SharedService,
{
provide: SHARED_CONFIG,
useValue: config
}
]);
}
// In bootstrapApplication (app root)
bootstrapApplication(AppComponent, {
providers: [
provideShared({ apiUrl: 'https://api.example.com' }),
provideRouter(routes),
provideHttpClient()
]
});
// In lazy-loaded routes â child-scope providers
export const routes: Routes = [
{
path: 'feature',
loadComponent: () => import('./feature.component'),
providers: [FeatureScopedService]
}
];
Multi-Providers
typescript
import { InjectionToken, inject } from '@angular/core';
export const HTTP_INTERCEPTORS =
new InjectionToken<HttpInterceptor[]>('http.interceptors');
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler) {
return next.handle(req);
}
}
@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler) {
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 {
private readonly interceptors = inject(HTTP_INTERCEPTORS);
}
Tree-Shakable Providers
typescript
// Tree-shakable (preferred) â service is removed if never injected
@Injectable({
providedIn: 'root'
})
export class NewService {}
Testing with DI
TestBed Provider Overrides
typescript
import { TestBed } from '@angular/core/testing';
describe('MyComponent', () => {
let mockUserService: jasmine.SpyObj<UserService>;
beforeEach(() => {
mockUserService = jasmine.createSpyObj('UserService', ['getUsers']);
TestBed.configureTestingModule({
imports: [MyComponent], // standalone component
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
typescript
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 route isolation
- Tree-shakable code
Angular DI Best Practices
- Use
inject()â Cleaner than constructor injection, works in class fields - Use
providedIn: 'root'â Tree-shakable and singleton - Use InjectionToken â Type-safe tokens, no string tokens
- Use standalone components â Providers in
@Component, not@NgModule - Use
provideX()functions â ReplacesforRoot()/forChild() - Use
inject()in factories â Nodepsarray needed - Use
inject()options â Replaces@Optional,@Self,@SkipSelf,@Host - Favor composition â Inject small, focused services
- Test with mocks â Override providers in TestBed
- Avoid circular dependencies â Refactor to a common service