On this page

Services and dependency injection

15 min read TextCh. 3 — Architecture

What is a service?

A service is a class that encapsulates reusable logic: data access, shared state, utilities, etc. It is the way to separate business logic from components.

Creating a service

Use the @Injectable decorator with providedIn: 'root' to create a singleton service:

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

@Injectable({ providedIn: 'root' })
export class CounterService {
  private readonly _value = signal(0);
  readonly value = this._value.asReadonly();

  increment(): void {
    this._value.update(v => v + 1);
  }
}

With providedIn: 'root', Angular creates a single instance of the service for the entire application.

Injecting a service

Use the inject() function to get an instance of the service:

import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { CounterService } from './counter.service';

@Component({
  selector: 'app-my-component',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <p>Value: {{ counter.value() }}</p>
    <button (click)="counter.increment()">+1</button>
  `,
})
export class MyComponent {
  readonly counter = inject(CounterService);
}

Injector hierarchy

Angular has an injector hierarchy. Each level can provide its own instances:

Root Injector (providedIn: 'root')
  └─ Component Injector (providers: [...])
       └─ Child Component Injector

Component-level service

If you need a unique instance per component (not a singleton), use providers:

@Component({
  selector: 'app-form',
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [FormStateService],  // New instance for each <app-form>
  template: `...`,
})
export class FormComponent {
  private readonly formState = inject(FormStateService);
}

Pattern: private state, public reading

A fundamental pattern is keeping the mutable signal private and exposing a read-only version:

@Injectable({ providedIn: 'root' })
export class AuthService {
  // Private: only the service can modify
  private readonly _user = signal<User | null>(null);

  // Public: components can only read
  readonly user = this._user.asReadonly();
  readonly isAuthenticated = computed(() => this._user() !== null);

  login(credentials: Credentials): void {
    // ... authentication logic
    this._user.set(authenticatedUser);
  }

  logout(): void {
    this._user.set(null);
  }
}

InjectionToken for values

To inject values that are not classes, use InjectionToken:

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

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

// Provide in the app config
export const appConfig = {
  providers: [
    { provide: API_URL, useValue: 'https://api.example.com' },
  ],
};

// Inject in a service
@Injectable({ providedIn: 'root' })
export class ApiService {
  private readonly baseUrl = inject(API_URL);
}

inject() in functions

inject() also works in functions that run in an injection context, such as guards and resolvers:

export function authGuard(): boolean {
  const auth = inject(AuthService);
  const router = inject(Router);

  if (auth.isAuthenticated()) return true;
  router.navigate(['/login']);
  return false;
}

Best practices

  1. One service, one responsibility — Don't create giant services
  2. Expose readonly — Use .asReadonly() for public signals
  3. inject() over constructor — More readable and flexible
  4. providedIn: 'root' — For most services

Practice

  1. Create a favorites service: Implement a FavoritesService with providedIn: 'root', a private signal for the list, and methods add(), remove(), and isFavorite(). Expose the list as asReadonly().
  2. Inject in two components: Use inject(FavoritesService) in two different components and verify they share the same instance (adding in one should be reflected in the other).
  3. Create an InjectionToken: Define an InjectionToken<string> for a base API URL, provide it in the app configuration, and use it in a service with inject().

In the next lesson, we will learn advanced routing: lazy loading, guards, resolvers, and nested layouts.

inject() over constructor
Prefer inject(MyService) over constructor injection. It is more concise, works with inheritance, and allows injecting in functions outside of classes.
Be careful with providedIn
Use providedIn: 'root' for singleton services. If you need one instance per component, use the providers array in the @Component decorator.
import { Injectable, inject, signal, computed } from '@angular/core';

// --- Model ---
interface Product {
  id: number;
  name: string;
  price: number;
}

interface CartItem {
  product: Product;
  quantity: number;
}

// --- Singleton service (providedIn: 'root') ---
@Injectable({ providedIn: 'root' })
export class CartService {
  private readonly items = signal<CartItem[]>([]);

  readonly cartItems = this.items.asReadonly();

  readonly totalItems = computed(
    () => this.items().reduce((sum, item) => sum + item.quantity, 0)
  );

  readonly totalPrice = computed(
    () => this.items().reduce(
      (sum, item) => sum + item.product.price * item.quantity, 0
    )
  );

  add(product: Product): void {
    this.items.update(list => {
      const existing = list.find(i => i.product.id === product.id);
      if (existing) {
        return list.map(i =>
          i.product.id === product.id
            ? { ...i, quantity: i.quantity + 1 }
            : i
        );
      }
      return [...list, { product, quantity: 1 }];
    });
  }

  remove(productId: number): void {
    this.items.update(list =>
      list.filter(i => i.product.id !== productId)
    );
  }

  clear(): void {
    this.items.set([]);
  }
}