Angular · Services

Services

A service is a class that does work for components: holds shared data, talks to an API, calculates prices. Components ask for services; services never touch the UI.

Intermediate 6 min Lesson 14 of 38Examples target Angular 20 with standalone components. Each lesson states the version it was verified against.
Before you start:Components
0/38 in Angular
On this page · 6 min · Intermediate

Learning objectives#

  • Create a service with @Injectable
  • Share state between components through a service
  • Expose read-only signals and methods
  • Decide what belongs in a service

Explanation#

Generate one with ng g service cart. @Injectable({ providedIn: "root" }) makes a single shared instance available everywhere.

Components get it with inject(CartService) (or as a constructor parameter).

Keep state private and expose it read-only: private items = signal<Item[]>([]); readonly items$ = this.items.asReadonly(). Changes go through methods (add, remove, clear), so rules live in one place.

Derived values (count, total) are computed() signals in the service, so every component sees the same numbers.

Good service candidates: API access, cart and session state, formatting rules, logging, feature flags. Bad candidates: anything about layout or DOM.

Syntax#

TypeScript Illustrative
@Injectable({ providedIn: 'root' })
export class CartService {
  private readonly items = signal<CartItem[]>([]);
  readonly lines = this.items.asReadonly();
  readonly total = computed(() => this.items().reduce((s, i) => s + i.price * i.qty, 0));
  add(item: CartItem) { … }
}

Code example: A cart service shared by three components#

TypeScriptcart.ts Illustrative
import { Component, Injectable, computed, inject, input, signal } from '@angular/core';
import { CurrencyPipe } from '@angular/common';


export type Product = { id: number; name: string; price: number };
type Line = Product & { qty: number };


@Injectable({ providedIn: 'root' })
export class CartService {
  private readonly items = signal<Line[]>([]);
  readonly lines = this.items.asReadonly();
  readonly count = computed(() => this.items().reduce((n, l) => n + l.qty, 0));
  readonly total = computed(() => this.items().reduce((s, l) => s + l.qty * l.price, 0));


  add(product: Product) {
    this.items.update(lines => {
      const found = lines.find(l => l.id === product.id);
      return found
        ? lines.map(l => (l.id === product.id ? { ...l, qty: l.qty + 1 } : l))
        : [...lines, { ...product, qty: 1 }];
    });
  }


  remove(id: number) {
    this.items.update(lines => lines.filter(l => l.id !== id));
  }
}


@Component({
  selector: 'app-add-button',
  template: `<button (click)="cart.add(product())">Add {{ product().name }}</button>`,
})
export class AddButton {
  product = input.required<Product>();
  protected cart = inject(CartService);
}


@Component({
  selector: 'app-cart-summary',
  imports: [CurrencyPipe],
  template: `
    <h3>Cart ({{ cart.count() }})</h3>
    @for (line of cart.lines(); track line.id) {
      <p>{{ line.name }} × {{ line.qty }} <button (click)="cart.remove(line.id)">✕</button></p>
    }
    <strong>{{ cart.total() | currency: 'INR' }}</strong>
  `,
})
export class CartSummary {
  protected cart = inject(CartService);
}


@Component({
  selector: 'app-shop',
  imports: [AddButton, CartSummary],
  template: `
    <app-add-button [product]="pen" />
    <app-add-button [product]="bag" />
    <app-cart-summary />
  `,
})
export class Shop {
  pen: Product = { id: 1, name: 'Pen', price: 20 };
  bag: Product = { id: 2, name: 'Bag', price: 650 };
}
Expected output
Clicking “Add Pen” twice and “Add Bag” once shows “Cart (3)”, “Pen × 2”, “Bag × 1” and “₹690.00”. Removing a line updates the count and total. The two buttons and the summary never talk to each other directly — they share CartService.

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. CartService keeps items private and exposes a read-only view plus computed count and total.
  2. add() contains the rule “increase quantity if already in the cart” — in one place.
  3. AddButton and CartSummary inject the same root instance.
  4. The summary reads signals from the service, so it updates automatically.

Real-world use case#

In an e-learning app, ProgressService tracks completed lessons; the sidebar, the lesson page and the dashboard all inject it and always agree on the learner’s progress.

Common mistakes#

Exposing writable state

If components can call service.items.set(...), rules are bypassed. Expose readonly signals and methods.

Providing a service in each component

providers: [CartService] in a component creates a separate copy for that subtree. Use providedIn: "root" for shared state.

DOM code in services

Services should not query or change the DOM. Keep them UI-free and testable.

Best practices#

  • One responsibility per service
  • Private writable signals, public readonly views
  • Derive with computed()
  • providedIn: "root" for app-wide singletons

Remember this#

Services = logic + shared state, no UI.

providedIn: "root" → one instance for the whole app.

Expose readonly signals; change through methods.

Practice exercise#

Your task: Write a NotificationService with a messages signal, push(text) and dismiss(index), and a component that lists messages with a dismiss button.

Need a hint?
messages.update(list => [...list, text]) and filter by index to dismiss.
Show solution
TypeScript Illustrative
import { Component, Injectable, inject, signal } from '@angular/core';


@Injectable({ providedIn: 'root' })
export class NotificationService {
  private readonly list = signal<string[]>([]);
  readonly messages = this.list.asReadonly();
  push(text: string) { this.list.update(l => [...l, text]); }
  dismiss(index: number) { this.list.update(l => l.filter((_, i) => i !== index)); }
}


@Component({
  selector: 'app-notifications',
  template: `
    <button (click)="notes.push('Saved!')">Notify</button>
    @for (m of notes.messages(); track $index) {
      <p>{{ m }} <button (click)="notes.dismiss($index)">✕</button></p>
    }
  `,
})
export class Notifications {
  protected notes = inject(NotificationService);
}

Mini quiz#

Services quizMultiple choice Question 1 of 3

What does @Injectable({ providedIn: "root" }) give you?

Score so far: 0/0

Want more? Practise all Angular MCQs · Angular interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.