Skip to main content
Services are singleton objects that provide specific functionality to your Angular application. They encapsulate business logic, data access, and other operations that can be shared across components.

What are Services?

Services are TypeScript classes that:
  • Contain reusable business logic
  • Manage application state
  • Handle data access (HTTP requests, local storage)
  • Provide utilities and helper functions
  • Enable communication between components
Services promote the single responsibility principle by separating business logic from presentation logic in components.

Creating a Service

Create a service using the @Injectable decorator:
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class ProductService {
  private products: Product[] = [];

  getProducts(): Product[] {
    return this.products;
  }

  addProduct(product: Product): void {
    this.products.push(product);
  }

  getProductById(id: number): Product | undefined {
    return this.products.find(p => p.id === id);
  }

  deleteProduct(id: number): void {
    this.products = this.products.filter(p => p.id !== id);
  }
}

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

Using Services in Components

Inject services using the inject() function:
import { Component, inject } from '@angular/core';
import { ProductService } from './product.service';

@Component({
  selector: 'product-list',
  standalone: true,
  template: `
    <h2>Products</h2>
    <ul>
      @for (product of products; track product.id) {
        <li>{{ product.name }} - {{ product.price | currency }}</li>
      }
    </ul>
    <button (click)="addNewProduct()">Add Product</button>
  `
})
export class ProductListComponent {
  private productService = inject(ProductService);
  products = this.productService.getProducts();

  addNewProduct() {
    this.productService.addProduct({
      id: Date.now(),
      name: 'New Product',
      price: 99.99
    });
    this.products = this.productService.getProducts();
  }
}

HTTP Data Service

Services commonly handle HTTP requests:
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, catchError, map, of } from 'rxjs';

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

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private http = inject(HttpClient);
  private apiUrl = 'https://api.example.com/users';

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl).pipe(
      catchError(this.handleError<User[]>('getUsers', []))
    );
  }

  getUser(id: number): Observable<User | undefined> {
    const url = `${this.apiUrl}/${id}`;
    return this.http.get<User>(url).pipe(
      catchError(this.handleError<User>('getUser'))
    );
  }

  createUser(user: Omit<User, 'id'>): Observable<User> {
    return this.http.post<User>(this.apiUrl, user).pipe(
      catchError(this.handleError<User>('createUser'))
    );
  }

  updateUser(user: User): Observable<User> {
    const url = `${this.apiUrl}/${user.id}`;
    return this.http.put<User>(url, user).pipe(
      catchError(this.handleError<User>('updateUser'))
    );
  }

  deleteUser(id: number): Observable<void> {
    const url = `${this.apiUrl}/${id}`;
    return this.http.delete<void>(url).pipe(
      catchError(this.handleError<void>('deleteUser'))
    );
  }

  private handleError<T>(operation = 'operation', result?: T) {
    return (error: any): Observable<T> => {
      console.error(`${operation} failed:`, error);
      return of(result as T);
    };
  }
}
Using the HTTP service:
import { Component, inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UserService } from './user.service';

@Component({
  selector: 'user-list',
  standalone: true,
  imports: [CommonModule],
  template: `
    @if (loading) {
      <p>Loading users...</p>
    } @else if (error) {
      <p class="error">{{ error }}</p>
    } @else {
      <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: User[] = [];
  loading = false;
  error: string | null = null;

  ngOnInit() {
    this.loadUsers();
  }

  loadUsers() {
    this.loading = true;
    this.userService.getUsers().subscribe({
      next: (users) => {
        this.users = users;
        this.loading = false;
      },
      error: (err) => {
        this.error = 'Failed to load users';
        this.loading = false;
      }
    });
  }
}
Always unsubscribe from observables or use the async pipe to prevent memory leaks.

State Management Service

Services can manage application state:
import { Injectable, signal, computed } from '@angular/core';

interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

@Injectable({
  providedIn: 'root'
})
export class CartService {
  // Using signals for reactive state
  private items = signal<CartItem[]>([]);
  
  // Computed values
  readonly itemCount = computed(() => 
    this.items().reduce((sum, item) => sum + item.quantity, 0)
  );
  
  readonly total = computed(() =>
    this.items().reduce((sum, item) => sum + (item.price * item.quantity), 0)
  );
  
  readonly cartItems = this.items.asReadonly();

  addItem(item: Omit<CartItem, 'quantity'>) {
    const currentItems = this.items();
    const existingItem = currentItems.find(i => i.id === item.id);
    
    if (existingItem) {
      this.items.set(
        currentItems.map(i => 
          i.id === item.id 
            ? { ...i, quantity: i.quantity + 1 }
            : i
        )
      );
    } else {
      this.items.set([...currentItems, { ...item, quantity: 1 }]);
    }
  }

  removeItem(id: number) {
    this.items.set(this.items().filter(item => item.id !== id));
  }

  updateQuantity(id: number, quantity: number) {
    if (quantity <= 0) {
      this.removeItem(id);
      return;
    }
    
    this.items.set(
      this.items().map(item =>
        item.id === id ? { ...item, quantity } : item
      )
    );
  }

  clear() {
    this.items.set([]);
  }
}
Using the state management service:
import { Component, inject } from '@angular/core';
import { CartService } from './cart.service';

@Component({
  selector: 'shopping-cart',
  standalone: true,
  template: `
    <h2>Shopping Cart</h2>
    <p>Items: {{ cartService.itemCount() }}</p>
    <p>Total: {{ cartService.total() | currency }}</p>
    
    @for (item of cartService.cartItems(); track item.id) {
      <div class="cart-item">
        <span>{{ item.name }}</span>
        <input 
          type="number" 
          [value]="item.quantity"
          (change)="updateQuantity(item.id, $event)">
        <span>{{ item.price * item.quantity | currency }}</span>
        <button (click)="cartService.removeItem(item.id)">Remove</button>
      </div>
    }
    
    <button (click)="cartService.clear()">Clear Cart</button>
  `
})
export class ShoppingCartComponent {
  cartService = inject(CartService);

  updateQuantity(id: number, event: Event) {
    const input = event.target as HTMLInputElement;
    const quantity = parseInt(input.value, 10);
    this.cartService.updateQuantity(id, quantity);
  }
}

Logger Service

Utility service for logging:
import { Injectable } from '@angular/core';

export enum LogLevel {
  Debug = 0,
  Info = 1,
  Warn = 2,
  Error = 3
}

@Injectable({
  providedIn: 'root'
})
export class LoggerService {
  private logLevel = LogLevel.Debug;

  setLogLevel(level: LogLevel) {
    this.logLevel = level;
  }

  debug(message: string, ...args: any[]) {
    if (this.logLevel <= LogLevel.Debug) {
      console.debug(`[DEBUG] ${message}`, ...args);
    }
  }

  info(message: string, ...args: any[]) {
    if (this.logLevel <= LogLevel.Info) {
      console.info(`[INFO] ${message}`, ...args);
    }
  }

  warn(message: string, ...args: any[]) {
    if (this.logLevel <= LogLevel.Warn) {
      console.warn(`[WARN] ${message}`, ...args);
    }
  }

  error(message: string, error?: any) {
    if (this.logLevel <= LogLevel.Error) {
      console.error(`[ERROR] ${message}`, error);
    }
  }
}

Communication Between Components

Use services to share data between unrelated components:
import { Injectable, signal } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class MessageService {
  private messageSignal = signal<string>('');
  readonly message = this.messageSignal.asReadonly();

  sendMessage(message: string) {
    this.messageSignal.set(message);
  }

  clearMessage() {
    this.messageSignal.set('');
  }
}

// Component A - sends message
@Component({
  selector: 'message-sender',
  standalone: true,
  template: `
    <input #input type="text">
    <button (click)="send(input.value)">Send</button>
  `
})
export class MessageSenderComponent {
  private messageService = inject(MessageService);

  send(message: string) {
    this.messageService.sendMessage(message);
  }
}

// Component B - receives message
@Component({
  selector: 'message-receiver',
  standalone: true,
  template: `
    <p>Received: {{ messageService.message() }}</p>
  `
})
export class MessageReceiverComponent {
  messageService = inject(MessageService);
}

Service with Dependencies

Services can depend on other services:
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { LoggerService } from './logger.service';
import { Observable, tap, catchError } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class ApiService {
  private http = inject(HttpClient);
  private logger = inject(LoggerService);
  private baseUrl = 'https://api.example.com';

  getData<T>(endpoint: string): Observable<T> {
    this.logger.info(`Fetching data from ${endpoint}`);
    
    return this.http.get<T>(`${this.baseUrl}/${endpoint}`).pipe(
      tap(data => this.logger.debug('Data received', data)),
      catchError(error => {
        this.logger.error(`Failed to fetch ${endpoint}`, error);
        throw error;
      })
    );
  }
}

Best Practices

Single Responsibility

Each service should have one clear purpose

Use providedIn: 'root'

For application-wide singletons

Return Observables

For asynchronous operations

Use Signals

For reactive state management
  1. Keep services focused - One responsibility per service
  2. Use signals for state - Reactive and performant
  3. Handle errors gracefully - Provide user-friendly error messages
  4. Document public APIs - Make services easy to understand
  5. Write unit tests - Services are easy to test in isolation
  6. Avoid circular dependencies - Structure services hierarchically

Next Steps

Dependency Injection

Deep dive into Angular’s DI system

HTTP Client

Learn about making HTTP requests