Skip to content
Angular ng services 4 min read

Dependency Injection Basics

Dependency injection (DI) is the design pattern at the heart of Angular: instead of a class creating the objects it depends on, it declares what it needs and Angular supplies them. This decouples your code, makes it trivial to swap implementations, and turns testing into a matter of providing fakes. Understanding the three moving parts — injectors, providers, and tokens — is the key to reasoning about how any dependency is resolved in your app.

The three pieces of DI

Angular’s injection system is built from a small vocabulary. Once you internalize these terms, every DI feature follows naturally.

TermWhat it is
TokenThe key used to look up a dependency. Usually a class, but can be an InjectionToken for non-class values.
ProviderA recipe that tells an injector how to create the value for a token.
InjectorA container that holds providers and resolves tokens into instances, caching them as singletons within its scope.
DependencyThe resolved value handed to whatever requested it.

When a component asks for a token, Angular walks its injector tree, finds the first matching provider, runs the recipe once, and reuses the result for that injector.

Declaring a dependency

The modern way to request a dependency is the inject() function, which works in constructors, field initializers, and factory functions. The older constructor-parameter style still works, but inject() is preferred in standalone Angular.

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

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

  getUsers() {
    return this.http.get<User[]>('/api/users');
  }
}

A component consumes that service the same way — it never calls new UserService():

import { Component, inject } from '@angular/core';
import { UserService } from './user.service';

@Component({
  selector: 'app-user-list',
  standalone: true,
  template: `
    @for (user of users(); track user.id) {
      <li>{{ user.name }}</li>
    } @empty {
      <li>No users yet.</li>
    }
  `,
})
export class UserListComponent {
  private userService = inject(UserService);
  users = this.userService.usersSignal;
}

Where providers live

A provider must be registered with an injector before its token can be resolved. There are three common places to do this, each producing a different scope.

Root injector with providedIn: 'root'

The most common choice. The service becomes an application-wide singleton and is tree-shakeable — if nothing injects it, the bundler drops it.

@Injectable({ providedIn: 'root' })
export class LoggerService {
  log(message: string) {
    console.log(`[app] ${message}`);
  }
}

Application config providers

For app-level wiring (HTTP, router, custom tokens) you register providers in bootstrapApplication:

import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(),
    LoggerService,
  ],
});

Component-level providers

Listing a provider in a component’s providers array creates a new instance scoped to that component and its children, replacing any ancestor instance for that subtree.

@Component({
  selector: 'app-cart',
  standalone: true,
  providers: [CartService], // fresh instance per <app-cart>
  template: `<p>Items: {{ count() }}</p>`,
})
export class CartComponent {
  private cart = inject(CartService);
  count = this.cart.itemCount;
}

How resolution travels the injector tree

Injectors form a hierarchy that mirrors your component tree, anchored by the root (and platform) injector at the top. When a class requests a token, Angular starts at the requesting element’s injector and walks upward until it finds a provider. The first match wins; if it reaches the root without a match, it throws a NullInjectorError.

NullInjectorError: No provider for CartService!

This bubbling is why a component-level provider only affects that branch — closer providers shadow ones higher up.

Tip: Because the first matching provider wins, you can override a root service for one feature area simply by re-providing the same token lower in the tree. This is the foundation of testing and theming.

Provider recipes

providedIn and bare class entries are shorthand for the useClass recipe. The full provider object lets you control exactly what a token resolves to.

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

export const API_URL = new InjectionToken<string>('API_URL');

export const appProviders = [
  // Value: a constant
  { provide: API_URL, useValue: 'https://api.example.com' },

  // Class: swap an implementation behind a token
  { provide: LoggerService, useClass: VerboseLoggerService },

  // Factory: compute the value, with its own injected deps
  {
    provide: AnalyticsService,
    useFactory: () => {
      const url = inject(API_URL);
      return new AnalyticsService(url);
    },
  },

  // Existing: alias one token to another instance
  { provide: OldLogger, useExisting: LoggerService },
];

Injecting the custom token works exactly like injecting a class:

@Injectable({ providedIn: 'root' })
export class AnalyticsService {
  private apiUrl = inject(API_URL);
}

Optional and self-scoped lookups

inject() accepts options that change resolution behavior — useful when a dependency might be absent or when you want to restrict the search.

const theme = inject(THEME, { optional: true }); // null instead of throwing
const localCfg = inject(Config, { self: true });  // only this injector
const parentCfg = inject(Config, { skipSelf: true }); // start at the parent

Best practices

  • Prefer providedIn: 'root' for app-wide services so they stay tree-shakeable and singleton.
  • Use the inject() function over constructor parameters in standalone code — it reads cleaner and works in field initializers.
  • Reach for component-level providers only when you genuinely need a fresh, isolated instance per component subtree.
  • Use InjectionToken for any non-class dependency (config strings, feature flags, function values) to keep lookups type-safe.
  • Favor useClass/useExisting to swap implementations in tests rather than mocking internals.
  • Keep services free of UI concerns so any injector in the tree can reuse them safely.
Last updated June 14, 2026
Was this helpful?