Services and Dependency Injection

@Injectable services, constructor and inject()-based DI, and singleton services with providedIn: root.

Why services exist

A component's job is to present data and handle user interaction — not to own business logic, fetch data directly, or hold state that multiple unrelated components need to share. Angular's convention for that other code is a service: a plain TypeScript class dedicated to one concern (fetching users, managing a shopping cart, logging), which components depend on rather than implementing that logic themselves.

Typescript
import { Injectable } from "@angular/core";

@Injectable({
  providedIn: "root",
})
export class UserService {
  private users = [
    { id: 1, name: "Ada Lovelace" },
    { id: 2, name: "Grace Hopper" },
  ];

  getUsers() {
    return this.users;
  }

  getUserById(id: number) {
    return this.users.find((u) => u.id === id);
  }
}

@Injectable() marks a class as available to Angular's dependency injection system. providedIn: "root" registers it as a singleton — one single shared instance exists for the entire application, and Angular creates it lazily the first time something actually asks for it.

Dependency injection, conceptually

Dependency injection (DI) means a class declares what it needs in its constructor, and something else — here, the Angular framework itself — is responsible for actually providing (constructing and passing in) that dependency. The class receiving it never calls new UserService() itself:

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

@Component({
  selector: "app-user-list",
  standalone: true,
  template: `
    <ul>
      <li *ngFor="let user of users">{{ user.name }}</li>
    </ul>
  `,
})
export class UserListComponent {
  private userService = inject(UserService);
  users = this.userService.getUsers();
}

The same thing written with the older, still very common constructor-injection style:

Typescript
import { Component } from "@angular/core";
import { UserService } from "./user.service";

@Component({
  selector: "app-user-list",
  standalone: true,
  template: `
    <ul>
      <li *ngFor="let user of users">{{ user.name }}</li>
    </ul>
  `,
})
export class UserListComponent {
  users: { id: number; name: string }[];

  constructor(private userService: UserService) {
    this.users = this.userService.getUsers();
  }
}

Both forms achieve the same thing: UserListComponent never constructs a UserService itself. It declares "I need a UserService," and Angular's injector resolves that dependency — creating the singleton instance if it doesn't already exist, or handing over the existing one if it does — and supplies it automatically. The newer inject() function (used inside a class field initializer, as shown first) is the more modern style; the constructor-parameter style is still extremely common in existing Angular code and works identically.

Why this matters: testability and decoupling

The real payoff of dependency injection is that UserListComponent depends only on UserService's public interface — getUsers(), getUserById() — not on how it's implemented. In a test, you can supply a fake UserService (with hardcoded test data and no real HTTP calls) instead of the real one, without touching UserListComponent's code at all:

Typescript
import { TestBed } from "@angular/core/testing";
import { UserService } from "./user.service";
import { UserListComponent } from "./user-list.component";

const fakeUserService = {
  getUsers: () => [{ id: 1, name: "Test User" }],
};

TestBed.configureTestingModule({
  imports: [UserListComponent],
  providers: [{ provide: UserService, useValue: fakeUserService }],
});

This is the same principle behind "program to an interface, not an implementation" in any object-oriented language — Angular's DI system just makes it the default, structurally encouraged way of writing components, rather than something you have to remember to do manually.

A more realistic example: a service used by a component

Typescript
// product.service.ts
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";

export interface Product {
  id: number;
  name: string;
  price: number;
}

@Injectable({
  providedIn: "root",
})
export class ProductService {
  private apiUrl = "/api/products";

  constructor(private http: HttpClient) {}

  getProducts(): Observable<Product[]> {
    return this.http.get<Product[]>(this.apiUrl);
  }
}
Typescript
// product-list.component.ts
import { Component, inject } from "@angular/core";
import { CommonModule } from "@angular/common";
import { ProductService } from "./product.service";

@Component({
  selector: "app-product-list",
  standalone: true,
  imports: [CommonModule],
  template: `
    <ul>
      <li *ngFor="let product of products$ | async">
        {{ product.name }} — \${{ product.price }}
      </li>
    </ul>
  `,
})
export class ProductListComponent {
  private productService = inject(ProductService);
  products$ = this.productService.getProducts();
}

ProductService itself depends on HttpClient — Angular's own built-in service for making HTTP requests — injected the same way ProductListComponent injects ProductService. Dependency injection chains naturally: a service can depend on other services, and Angular resolves the whole graph automatically. HttpClient and the products$ | async pattern are covered in full on the next page.

Common mistakes

  • Constructing a service manually with new UserService() instead of injecting it — this bypasses Angular's DI system entirely, silently creating a second, separate instance instead of sharing the intended singleton.
  • Putting business logic or HTTP calls directly inside a component instead of a service — makes the logic untestable in isolation and impossible to reuse from another component.
  • Forgetting providedIn: "root" (or otherwise registering the service with a provider) — without it, Angular has no configured way to create the service when something requests it.