AngularJSDependency Injection

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:

TS
// 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() {}
}

TS
// 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:

Text
Root Injector (providedIn: 'root')
  └── Platform Injector (providedIn: 'platform')
        └── App Injector (bootstrapApplication / AppModule)
              └── Feature Module Injector
                    └── Component Injector
                          └── Child Component Injector

When a component needs a service, Angular:

  1. Checks the component's own injector
  2. If not found, walks up to the parent component's injector
  3. Continues up the tree until reaching the root
  4. Throws a NullInjectorError if no provider is found
Note
This lookup mechanism means a service provided at a higher level is shared by all descendants. A service provided at the component level creates an isolated instance.
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:

TS
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']);
    }
  }
}
Tip
Use 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:

TS
@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

TS
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

TS
// 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:

TS
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:

TS
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:

TS
// 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

Warning
If you forget @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 @Host control 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.