Services & Dependency Injection

Angular Services

Learn how to create and use Angular services to share data and logic across components, and understand the singleton service pattern.

What Are Services?

Services are classes that encapsulate reusable logic — data fetching, business rules, state management, logging, and more. They keep your components focused on the view while services handle everything else.

Why Use Services?

  • Separation of concerns — components handle the UI, services handle logic
  • Code reuse — multiple components share the same service
  • Testability — services are easy to unit test
  • Singleton pattern — one instance shared across the entire application

Creating a Service

Using the CLI

bash
ng generate service services/user
# Shorthand: ng g s services/user

Manual Creation

typescript
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class UserService {
  private users = [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
    { id: 2, name: 'Bob', email: 'bob@example.com' },
    { id: 3, name: 'Charlie', email: 'charlie@example.com' },
  ];

  getUsers() {
    return this.users;
  }

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

  addUser(user: { name: string; email: string }) {
    const newUser = { ...user, id: this.users.length + 1 };
    this.users.push(newUser);
    return newUser;
  }
}

The @Injectable Decorator

typescript
@Injectable({
  providedIn: 'root', // Available application-wide as a singleton
})
providedIn ValueScope
'root'Application-wide singleton (most common)
'platform'Shared across multiple apps on the page
'any'Unique instance per lazy-loaded module

Using a Service in a Component

With inject() Function (Modern)

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

@Component({
  selector: 'app-user-list',
  standalone: true,
  template: `
    <h2>Users</h2>
    <ul>
      @for (user of users; track user.id) {
        <li>{{ user.name }} ({{ user.email }})</li>
      }
    </ul>
  `,
})
export class UserListComponent implements OnInit {
  private userService = inject(UserService);
  users: { id: number; name: string; email: string }[] = [];

  ngOnInit() {
    this.users = this.userService.getUsers();
  }
}

With Constructor Injection (Classic)

typescript
@Component({
  selector: 'app-user-list',
  standalone: true,
  template: `<!-- same as above -->`,
})
export class UserListComponent implements OnInit {
  users: { id: number; name: string; email: string }[] = [];

  constructor(private userService: UserService) {}

  ngOnInit() {
    this.users = this.userService.getUsers();
  }
}

Recommendation: Use the inject() function for new code — it works in components, directives, pipes, guards, and resolver functions.

Practical Service Examples

Logger Service

typescript
@Injectable({ providedIn: 'root' })
export class LoggerService {
  private logs: string[] = [];

  log(message: string) {
    const timestamp = new Date().toISOString();
    const entry = `[${timestamp}] ${message}`;
    this.logs.push(entry);
    console.log(entry);
  }

  warn(message: string) {
    console.warn(`⚠️ ${message}`);
  }

  error(message: string) {
    console.error(`❌ ${message}`);
  }

  getLogs(): string[] {
    return [...this.logs];
  }
}

Shopping Cart Service

typescript
interface CartItem {
  productId: number;
  name: string;
  price: number;
  quantity: number;
}

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

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

  addItem(product: { id: number; name: string; price: number }) {
    const existing = this.items.find(i => i.productId === product.id);
    if (existing) {
      existing.quantity++;
    } else {
      this.items.push({
        productId: product.id,
        name: product.name,
        price: product.price,
        quantity: 1,
      });
    }
  }

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

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

  getItemCount(): number {
    return this.items.reduce((sum, item) => sum + item.quantity, 0);
  }

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

Notification Service

typescript
export interface Notification {
  id: number;
  message: string;
  type: 'success' | 'error' | 'info' | 'warning';
  timestamp: Date;
}

@Injectable({ providedIn: 'root' })
export class NotificationService {
  private notifications: Notification[] = [];
  private nextId = 1;

  show(message: string, type: Notification['type'] = 'info') {
    const notification: Notification = {
      id: this.nextId++,
      message,
      type,
      timestamp: new Date(),
    };
    this.notifications.push(notification);

    // Auto-dismiss after 5 seconds
    setTimeout(() => this.dismiss(notification.id), 5000);
  }

  success(message: string) { this.show(message, 'success'); }
  error(message: string) { this.show(message, 'error'); }
  warning(message: string) { this.show(message, 'warning'); }

  dismiss(id: number) {
    this.notifications = this.notifications.filter(n => n.id !== id);
  }

  getAll(): Notification[] {
    return [...this.notifications];
  }
}

Service-to-Service Communication

Services can inject other services:

typescript
@Injectable({ providedIn: 'root' })
export class OrderService {
  private cartService = inject(CartService);
  private notificationService = inject(NotificationService);
  private loggerService = inject(LoggerService);

  placeOrder() {
    const items = this.cartService.getItems();
    if (items.length === 0) {
      this.notificationService.warning('Cart is empty!');
      return;
    }

    const total = this.cartService.getTotal();
    this.loggerService.log(`Order placed: ${items.length} items, $${total}`);
    this.cartService.clear();
    this.notificationService.success('Order placed successfully!');
  }
}

Services with Observables

For reactive data, services often expose Observables using BehaviorSubject:

typescript
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class ThemeService {
  private themeSubject = new BehaviorSubject<'light' | 'dark'>('light');
  theme$: Observable<'light' | 'dark'> = this.themeSubject.asObservable();

  toggleTheme() {
    const current = this.themeSubject.value;
    this.themeSubject.next(current === 'light' ? 'dark' : 'light');
  }

  setTheme(theme: 'light' | 'dark') {
    this.themeSubject.next(theme);
  }
}
typescript
// In a component
export class AppComponent {
  private themeService = inject(ThemeService);
  theme$ = this.themeService.theme$;

  toggleTheme() {
    this.themeService.toggleTheme();
  }
}
html
<div [class]="(theme$ | async) === 'dark' ? 'dark-mode' : 'light-mode'">
  <button (click)="toggleTheme()">Toggle Theme</button>
</div>

Best Practices

  1. Use providedIn: 'root' for most services — creates a tree-shakable singleton
  2. Keep services focused — one service per concern (auth, cart, logging)
  3. Return new arrays/objects for immutability (return [...this.items])
  4. Use interfaces for data models, not the service class
  5. Prefer inject() over constructor injection for cleaner code

Next Steps

Services need to be made available to components through dependency injection — Angular's powerful DI system. Let's explore how it works under the hood.