Dependency Injection

This section documents modern, standalone Angular — signals, the built-in @if / @for / @switch control flow, @defer, typed reactive forms, provideHttpClient, functional guards and interceptors, and server-side rendering with hydration — as described by the official documentation at angular.dev, which is the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against angular.dev before being relied on in production. Angular ships a major release roughly every six months and its APIs continue to evolve: the examples here target the current major release; where a consulted source disagrees with the current documentation, the documentation wins and the difference is noted.

This section’s bibliography lists the reference material consulted while preparing these pages.

Dependency injection (DI) is how an Angular class asks for the objects it needs instead of constructing them. A tree of injectors resolves each request, so services are shared, swappable, and easy to fake in tests.

Services and providedIn: 'root'

A service is a plain class marked with @Injectable. Setting providedIn: 'root' registers it with the application’s root injector, making it a singleton that any component or other service can inject, and letting the bundler tree-shake it away when nothing uses it.

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({ providedIn: 'root' })
export class BookService {
  private readonly http = inject(HttpClient);

  list() {
    return this.http.get<Book[]>('/api/books');
  }
}

Consume it by injecting the class token:

import { Component, inject } from '@angular/core';

@Component({
  selector: 'app-book-list',
  template: `@for (book of books(); track book.id) { <li>{{ book.title }}</li> }`,
})
export class BookList {
  private readonly service = inject(BookService);
  readonly books = toSignal(this.service.list(), { initialValue: [] as Book[] });
}

@Injectable(\{ providedIn: 'root' }) is the default the CLI generates with ng generate service book. See Dependency injection and Creating and using services.

inject() versus constructor injection

Both forms retrieve the same instance from the same injector. inject() is a function call in a field initializer; constructor injection uses a typed parameter with an implicit @Inject for the type.

import { Injectable, inject } from '@angular/core';

// preferred: inject() in a field
@Injectable({ providedIn: 'root' })
export class ReportService {
  private readonly http = inject(HttpClient);
  private readonly books = inject(BookService);
}

// equivalent: constructor parameters
@Injectable({ providedIn: 'root' })
export class LegacyReportService {
  constructor(
    private readonly http: HttpClient,
    private readonly books: BookService,
  ) {}
}

inject() composes better: it works inside base classes without threading constructor parameters through subclasses, it needs no constructor at all, and it gives functions (functional guards, interceptors, resolvers) access to DI. It only works within an injection context.

The injection context

inject() may be called only while Angular is executing constructor-time code: class field initializers and constructors of injectables, factory functions passed to useFactory or InjectionToken, and the callbacks of runInInjectionContext. Calling it later — for example inside an event handler or a setTimeout — throws NG0203.

To reach DI outside that window, capture an Injector (or an EnvironmentInjector) while you still have a context and re-enter it with runInInjectionContext:

import { Component, EnvironmentInjector, inject, runInInjectionContext } from '@angular/core';

@Component({ selector: 'app-widget', template: '' })
export class Widget {
  private readonly injector = inject(EnvironmentInjector);

  onClick() {
    runInInjectionContext(this.injector, () => {
      const books = inject(BookService); // now valid
      books.list();
    });
  }
}

Provider recipes

A provider tells an injector how to produce the value for a token. providedIn uses the class itself as both token and recipe; an explicit providers entry can decouple the two.

import { InjectionToken } from '@angular/core';

export interface AppConfig {
  apiUrl: string;
  pageSize: number;
}

export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');

export const providers = [
  // useClass: satisfy a token with a (possibly different) class
  { provide: BookService, useClass: FakeBookService },

  // useValue: a ready-made constant (config, tokens, test doubles)
  { provide: APP_CONFIG, useValue: { apiUrl: '/api', pageSize: 20 } },

  // useFactory: compute the value, optionally from other dependencies
  {
    provide: 'BOOKS_ENDPOINT',
    useFactory: (cfg: AppConfig) => `${cfg.apiUrl}/books`,
    deps: [APP_CONFIG],
  },

  // useExisting: an alias -- return the instance already registered for another token
  { provide: LoggerApi, useExisting: ConsoleLogger },

  // multi: contribute one entry to an array token instead of replacing it
  { provide: VALIDATORS, useClass: LengthValidator, multi: true },
  { provide: VALIDATORS, useClass: PatternValidator, multi: true },
];

Inject a non-class token with inject(TOKEN):

const config = inject(APP_CONFIG);        // typed as AppConfig
const validators = inject(VALIDATORS);    // typed as Validator[] because of multi

InjectionToken gives a type-safe key for values that have no class to name them (configuration objects, strings, feature flags). Provide a factory in its options to make it tree-shakable and root-scoped by default. See Configuring dependency providers.

Hierarchical injectors

Injectors form a tree with two parallel branches. Angular resolves a token by walking up from the injector where the request originates until it finds a provider, then throws NG0201 if it reaches the root without a match.

  • Environment injectors — the root injector created by bootstrapApplication, plus a child for every lazy route that declares providers. Services with providedIn: 'root' and everything in ApplicationConfig live here.

  • Element injectors — one per component/directive host element, configured by the component’s providers and viewProviders metadata. A child element injector delegates to its parent element injector, and the outermost element injector delegates to the environment injector.

import { Component } from '@angular/core';

@Component({
  selector: 'app-cart',
  providers: [CartService],        // a fresh CartService for this component and its whole subtree
  viewProviders: [PricingService], // visible to this component's view, but NOT to projected <ng-content>
  template: `<app-cart-line />`,
})
export class Cart {}

Component-level providers create a new instance scoped to that component instance and its descendants — useful for per-feature or per-dialog state. viewProviders is the same but hides the provider from content projected into the component.

Resolution modifiers

Parameter decorators change how far the lookup walks:

import { Component, Host, Optional, Self, SkipSelf, inject } from '@angular/core';

@Component({ selector: 'app-cart-line', template: '' })
export class CartLine {
  // inject() equivalents take an options object
  private readonly own = inject(CartService, { self: true });          // only this element injector
  private readonly parent = inject(CartService, { skipSelf: true });   // start at the parent
  private readonly maybe = inject(PricingService, { optional: true }); // null instead of throwing
  private readonly host = inject(CartService, { host: true });         // stop at the host component

  // decorator form (constructor injection)
  constructor(@Optional() @Self() svc: CartService | null) {}
}

@Self / \{ self: true } restricts the search to the current injector; @SkipSelf starts one level up; @Host stops at the host component’s element injector; @Optional / \{ optional: true } yields null rather than an error when nothing is found.

Lazy-loaded routes get their own environment injector, so providers on a lazy route (or via providers in a route config) are singletons for that lazily loaded area only — a practical way to scope a service to a feature without making it global. See Hierarchical injectors.

The injector tree

Environment injectors on the left — the root injector and a lazy-route child — and element injectors on the right at each nested component host; a dependency request walks up its own element-injector branch and then into the environment branch until a provider is found

A request raised in a deeply nested component checks that component’s element injector, then each ancestor element injector, then the environment injector of its lazy route (if any), and finally the root environment injector — taking the first provider it meets.