Angular: Zero to Senior · Lesson 4 of 11

Services & Dependency Injection

Services are where your business logic and data access live. Components should be thin — they display data and respond to user events. Services do the heavy lifting. Angular's dependency injection system wires everything together automatically.


Creating a Service

Bash
ng g service services/user
TYPESCRIPT
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',  // singleton — one instance for the whole app
})
export class UserService {
  private users: User[] = [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
    { id: 2, name: 'Bob',   email: 'bob@example.com'   },
  ];

  getAll(): User[] {
    return this.users;
  }

  getById(id: number): User | undefined {
    return this.users.find(u => u.id === id);
  }

  add(user: Omit<User, 'id'>): User {
    const newUser = { ...user, id: Date.now() };
    this.users.push(newUser);
    return newUser;
  }
}

export interface User {
  id: number;
  name: string;
  email: string;
}

Injecting a Service

Constructor Injection (classic)

TYPESCRIPT
import { Component, OnInit } from '@angular/core';
import { UserService, User } from '../services/user.service';

@Component({
  selector: 'app-user-list',
  standalone: true,
  template: `
    @for (user of users; track user.id) {
      <p>{{ user.name }}</p>
    }
  `,
})
export class UserListComponent implements OnInit {
  users: User[] = [];

  constructor(private userService: UserService) {}

  ngOnInit(): void {
    this.users = this.userService.getAll();
  }
}

inject() Function (modern — Angular 14+)

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

@Component({ /* ... */ })
export class UserListComponent implements OnInit {
  private userService = inject(UserService);  // no constructor needed
  users = this.userService.getAll();          // can call immediately

  ngOnInit(): void {
    // already populated above
  }
}

inject() is now the preferred approach in modern Angular. It works in components, directives, pipes, and other services. It cannot be called outside of an injection context (inside regular class methods or setTimeout, for example).


providedIn: 'root' — What It Means

providedIn: 'root' registers the service with the root injector. Angular creates one instance and shares it across the entire application. This is what you want for most services.

TYPESCRIPT
@Injectable({ providedIn: 'root' })
export class AuthService { /* ... */ }

This is tree-shakeable — if no component injects AuthService, it won't be included in the production bundle.


Service Lifetimes in Angular

| Registration | Scope | Use for | |---|---|---| | providedIn: 'root' | Entire app (singleton) | Shared state, HTTP services, auth | | providers: [MyService] in component | Component + children | Isolated service per component | | providedIn: 'platform' | Multiple app instances | Shared between micro-frontends |

Component-scoped Service

When you provide a service in a component's providers, each instance of that component gets its own service instance — independent of other components:

TYPESCRIPT
@Component({
  selector: 'app-shopping-cart',
  standalone: true,
  providers: [CartService],  // each <app-shopping-cart> gets its own CartService
  template: `...`,
})
export class ShoppingCartComponent {
  private cart = inject(CartService);
}

Services Calling Services

Services can inject other services — this is the normal pattern:

TYPESCRIPT
@Injectable({ providedIn: 'root' })
export class OrderService {
  private http    = inject(HttpClient);
  private auth    = inject(AuthService);
  private toaster = inject(ToastService);

  async placeOrder(order: CreateOrderRequest): Promise<Order> {
    const token = this.auth.getToken();
    const result = await firstValueFrom(
      this.http.post<Order>('/api/orders', order, {
        headers: { Authorization: `Bearer ${token}` },
      })
    );
    this.toaster.success('Order placed!');
    return result;
  }
}

Sharing State Between Components with a Service

Services are the simplest way to share state — inject the same singleton into multiple components:

TYPESCRIPT
@Injectable({ providedIn: 'root' })
export class CartService {
  private items: CartItem[] = [];

  add(product: Product, qty = 1): void {
    const existing = this.items.find(i => i.productId === product.id);
    if (existing) {
      existing.quantity += qty;
    } else {
      this.items.push({ productId: product.id, name: product.name, quantity: qty, price: product.price });
    }
  }

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

  getItems(): CartItem[] {
    return [...this.items];
  }

  get total(): number {
    return this.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
  }

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

Any component that injects CartService reads and modifies the same array — no prop-drilling needed.

For reactive state (components auto-updating when service state changes), use BehaviorSubject or Angular Signals:

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

@Injectable({ providedIn: 'root' })
export class CartService {
  private _items = signal<CartItem[]>([]);

  items   = this._items.asReadonly();
  total   = computed(() => this._items().reduce((sum, i) => sum + i.price * i.quantity, 0));
  count   = computed(() => this._items().reduce((sum, i) => sum + i.quantity, 0));

  add(item: CartItem): void {
    this._items.update(items => [...items, item]);
  }

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

Components using signals automatically re-render when items or total change.


InjectionToken — Inject Non-Class Values

Sometimes you need to inject a config object, string, or factory — not a class. Use InjectionToken:

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

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

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

Provide it in app.config.ts:

TYPESCRIPT
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    {
      provide: APP_CONFIG,
      useValue: { apiUrl: 'https://api.myapp.com', maxRetries: 3 },
    },
  ],
};

Inject it anywhere:

TYPESCRIPT
@Injectable({ providedIn: 'root' })
export class ApiService {
  private config = inject(APP_CONFIG);

  getApiUrl(path: string): string {
    return `${this.config.apiUrl}${path}`;
  }
}

Testing Services

Services with inject() are easy to unit test:

TYPESCRIPT
import { TestBed } from '@angular/core/testing';
import { CartService } from './cart.service';

describe('CartService', () => {
  let service: CartService;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(CartService);
  });

  it('should add items and calculate total', () => {
    service.add({ productId: 1, name: 'Widget', quantity: 2, price: 10 });
    expect(service.getItems().length).toBe(1);
    expect(service.total).toBe(20);
  });

  it('should clear the cart', () => {
    service.add({ productId: 1, name: 'Widget', quantity: 1, price: 10 });
    service.clear();
    expect(service.getItems().length).toBe(0);
  });
});

Quick Reference

Create service:       ng g s services/name
Singleton (app-wide): @Injectable({ providedIn: 'root' })
Inject (modern):      private svc = inject(ServiceName)
Inject (classic):     constructor(private svc: ServiceName) {}
Component-scoped:     providers: [ServiceName] in @Component
Reactive state:       signal() + computed() in service
Inject config:        InjectionToken + provide: { provide: TOKEN, useValue }
Testing:              TestBed.inject(ServiceName)