Dependency Injection
Dependency Injection (DI) is a design pattern in which a class receives its dependencies from an external source rather than creating them itself. Angular has a built-in DI framework that is central to how the entire framework operates — every service, pipe, directive, and component participates in it.
DI makes code:
- Testable — swap real dependencies with mocks in tests
- Maintainable — change how a dependency is created without touching consumers
- Modular — decouple classes from their concrete implementations
The Problem DI Solves
Without DI, a component directly instantiates its dependencies:
// Without DI — tightly coupled, hard to test
class OrderComponent {
private userService = new UserService(); // hard-coded dependency
private http = new HttpClient(/* ... */); // how do you mock this in tests?
private logger = new Logger('OrderComponent'); // different config impossible
constructor() {}
}// With DI — loosely coupled, easily testable
@Component({ selector: 'app-order' })
class OrderComponent {
// Angular creates and provides the dependencies
constructor(
private userService: UserService,
private http: HttpClient,
private logger: Logger,
) {}
}The Angular Injector Tree
Angular maintains a tree of injectors that mirrors the component tree. When a component requests a dependency, Angular traverses up the injector tree until it finds a provider:
Root Injector (providedIn: 'root')
└── Platform Injector (providedIn: 'platform')
└── App Injector (bootstrapApplication / AppModule)
└── Feature Module Injector
└── Component Injector
└── Child Component InjectorWhen a component needs a service, Angular:
- Checks the component's own injector
- If not found, walks up to the parent component's injector
- Continues up the tree until reaching the root
- Throws a
NullInjectorErrorif no provider is found
The inject() Function
Angular 14 introduced the inject() function as a more powerful alternative to constructor injection. It can be used in constructors, class field initializers, and any function called within an injection context:
import { inject, Component, OnInit } from '@angular/core';
import { UserService } from './user.service';
import { Router } from '@angular/router';
import { Title } from '@angular/platform-browser';
@Component({ selector: 'app-dashboard' })
export class DashboardComponent implements OnInit {
// Inject in field initializer (most concise)
private userService = inject(UserService);
private router = inject(Router);
private title = inject(Title);
ngOnInit() {
this.title.setTitle('Dashboard');
if (!this.userService.isLoggedIn()) {
this.router.navigate(['/login']);
}
}
}inject() for field-level initialization — it removes boilerplate constructor parameters and makes dependencies immediately available as typed fields.Constructor Injection
Traditional constructor injection remains valid and widely used:
@Injectable({ providedIn: 'root' })
export class PaymentService {
constructor(
private http: HttpClient,
private logger: LoggingService,
private config: AppConfigService,
) {}
processPayment(amount: number): Observable<PaymentResult> {
this.logger.log(`Processing payment: ${amount}`);
return this.http.post<PaymentResult>('/api/payments', {
amount,
currency: this.config.get('defaultCurrency'),
});
}
}@Optional, @Self, @SkipSelf, @Host
Angular provides decorators (and equivalent inject() options) to control the DI resolution behavior:
Decorator | inject() equivalent | Behavior |
|---|---|---|
@Optional() | inject(Token, { optional: true }) | Returns null instead of throwing if not found |
@Self() | inject(Token, { self: true }) | Only looks in the component's own injector |
@SkipSelf() | inject(Token, { skipSelf: true }) | Skips the component's own injector, goes straight to parent |
@Host() | inject(Token, { host: true }) | Stops searching at the host element's injector |
import { Optional, Self, SkipSelf, Host, inject } from '@angular/core';
// Constructor-style
@Component({ selector: 'app-child' })
export class ChildComponent {
constructor(
@Optional() private theme: ThemeService, // null if not provided
@Self() private validator: ValidatorService, // only own injector
@SkipSelf() private parent: ParentService, // skip own, go to parent
@Host() private host: HostService, // stop at host
) {}
}
// inject()-style (Angular 14+)
@Component({ selector: 'app-child' })
export class ChildComponent {
private theme = inject(ThemeService, { optional: true }); // null if missing
private validator = inject(ValidatorService, { self: true });
private parent = inject(ParentService, { skipSelf: true });
}Providing Services at Different Levels
// 1. Root level — singleton for the entire app (most common)
@Injectable({ providedIn: 'root' })
export class AuthService {}
// 2. Component level — new instance per component
@Component({
selector: 'app-cart',
providers: [CartService], // isolated CartService for this component tree
})
export class CartComponent {}
// 3. Route level — new instance per route activation
export const routes: Routes = [
{
path: 'checkout',
component: CheckoutComponent,
providers: [CheckoutService], // scoped to this route
},
];
// 4. Lazy module level — new instance for the module
@NgModule({
providers: [FeatureService], // available within this module only
})
export class FeatureModule {}Using DI for Configuration
A powerful DI pattern is injecting configuration values using InjectionToken. This avoids hardcoding values and makes testing easier:
import { InjectionToken, inject } from '@angular/core';
// Define the token
export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL');
// Provide it at bootstrap
bootstrapApplication(AppComponent, {
providers: [
{ provide: API_BASE_URL, useValue: 'https://api.myapp.com' },
],
});
// Inject it anywhere
@Injectable({ providedIn: 'root' })
export class HttpService {
private baseUrl = inject(API_BASE_URL);
get<T>(path: string): Observable<T> {
return this.http.get<T>(`${this.baseUrl}${path}`);
}
}DI in Angular Testing
DI makes testing straightforward — replace real services with mocks by providing an alternative implementation:
import { TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
describe('DashboardComponent', () => {
let component: DashboardComponent;
const mockUserService = {
getAll: () => of([{ id: 1, name: 'Alice' }]),
isLoggedIn: () => true,
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [DashboardComponent],
providers: [
// Replace the real service with the mock
{ provide: UserService, useValue: mockUserService },
],
});
component = TestBed.createComponent(DashboardComponent).componentInstance;
});
it('should load users', () => {
expect(component.users().length).toBe(1);
});
});DI Without Decorators — Angular 17 Signals Approach
Angular's modern pattern uses functional-style injection with inject() and signals, requiring no decorators on services for simple cases:
// Modern service with inject() and signals — no constructor needed
import { Injectable, signal, computed, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';
@Injectable({ providedIn: 'root' })
export class ProductStore {
private http = inject(HttpClient);
private _products = signal<Product[]>([]);
private _loading = signal(false);
private _filter = signal('');
readonly products = this._products.asReadonly();
readonly loading = this._loading.asReadonly();
readonly filteredProducts = computed(() =>
this._products().filter(p =>
p.name.toLowerCase().includes(this._filter().toLowerCase())
)
);
loadProducts() {
this._loading.set(true);
this.http.get<Product[]>('/api/products').subscribe({
next: products => {
this._products.set(products);
this._loading.set(false);
},
});
}
setFilter(term: string) {
this._filter.set(term);
}
}Common DI Errors and Solutions
NullInjectorError: No provider for X — add X to providers, or use providedIn in its @Injectable decorator
Circular dependency error — service A injects B which injects A; refactor to break the cycle with a third service
Can't resolve all parameters for X — missing @Injectable decorator or using class not registered with DI
NG0201: No provider for X found in NodeInjector — service provided in a lazy module not yet loaded, or @Self() used incorrectly
Getting the wrong instance — check if a service is provided at multiple levels (root + component); component-level takes precedence
@Injectable() on a service class, Angular cannot inject it and you will get a runtime error. Always add the decorator, even if the class has no dependencies of its own.Summary
Angular's DI system is powerful and flexible:
- Angular walks the injector tree upward to resolve dependencies
- Use
inject()for clean field-level injection in Angular 14+ - Use constructor injection for compatibility and explicitness
@Optional,@Self,@SkipSelf, and@Hostcontrol resolution scope- DI makes testing trivial — mock any dependency by providing an alternative
Next: explore the Providers system to see all the ways you can register dependencies.