AngularJSServices

Services in Angular

A service in Angular is a class that encapsulates reusable business logic, data access, or shared state that does not belong to any single component. Services are the primary mechanism for sharing data and behavior between components that have no direct parent-child relationship.

The key principle: components should be thin. They handle display and user interactions. Services handle everything else — HTTP calls, caching, validation logic, and cross-component state.

Creating a Service

Use the Angular CLI to generate a service:

Bash
ng generate service services/user
# or shorthand
ng g s services/user
CREATE src/app/services/user.service.spec.ts
CREATE src/app/services/user.service.ts

The generated service looks like this:

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

@Injectable({
  providedIn: 'root', // register service with the root injector
})
export class UserService {
  // your business logic goes here
}
Note
The @Injectable decorator marks the class as a participant in Angular's dependency injection system. The providedIn: 'root' metadata registers it as a singleton available application-wide.
A Real-World Service Example

Here is a complete UserService that encapsulates all user-related operations:

TS
import { Injectable, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, tap, catchError, throwError } from 'rxjs';

export interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
}

@Injectable({
  providedIn: 'root',
})
export class UserService {
  private readonly apiUrl = '/api/users';

  // Shared reactive state using signals
  private _currentUser = signal<User | null>(null);
  readonly currentUser = this._currentUser.asReadonly();

  constructor(private http: HttpClient) {}

  getAll(): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl);
  }

  getById(id: number): Observable<User> {
    return this.http.get<User>(`${this.apiUrl}/${id}`);
  }

  create(data: Omit<User, 'id'>): Observable<User> {
    return this.http.post<User>(this.apiUrl, data);
  }

  update(id: number, changes: Partial<User>): Observable<User> {
    return this.http.patch<User>(`${this.apiUrl}/${id}`, changes);
  }

  delete(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`);
  }

  login(email: string, password: string): Observable<User> {
    return this.http.post<User>('/api/auth/login', { email, password }).pipe(
      tap(user => this._currentUser.set(user)),
      catchError(err => throwError(() => new Error('Login failed: ' + err.message)))
    );
  }

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

  isAdmin(): boolean {
    return this._currentUser()?.role === 'admin';
  }
}
Injecting Services into Components

Angular 14+ offers two equivalent ways to inject services. Both produce identical behavior:

TS
// Method 1: Constructor injection (traditional)
@Component({ standalone: true, imports: [AsyncPipe] })
export class UserListComponent {
  users$: Observable<User[]>;

  constructor(private userService: UserService) {
    this.users$ = this.userService.getAll();
  }
}

// Method 2: inject() function (modern, preferred in Angular 14+)
import { inject } from '@angular/core';

@Component({ standalone: true, imports: [AsyncPipe] })
export class UserListComponent {
  private userService = inject(UserService);
  users$ = this.userService.getAll();
}
Tip
The inject() function is preferred in modern Angular because it works in constructors, field initializers, and also inside functions called from a component's constructor context — making it more flexible than constructor parameter injection.
Service Lifetime and Scope

Where you provide a service determines when it is instantiated and how long it lives:

providedIn value

Scope

Instances

'root'

Entire application

Single instance (singleton)

'platform'

Multiple apps on the same page

One per platform

'any'

Each lazy-loaded module gets its own

Multiple possible

Component providers[]

Component and its children

One per component instance

Module providers[]

Module and its components

One per module

TS
// Scoped to a specific component tree
@Component({
  selector: 'app-checkout',
  providers: [CartService], // new CartService instance for each CheckoutComponent
})
export class CheckoutComponent {
  // This CartService instance is destroyed when CheckoutComponent is destroyed
  private cart = inject(CartService);
}
Sharing State Between Components

One of the most common uses of services is sharing state between sibling components. Using signals (Angular 16+):

TS
// notification.service.ts
import { Injectable, signal, computed } from '@angular/core';

export interface Notification {
  id: string;
  message: string;
  type: 'success' | 'error' | 'info';
}

@Injectable({ providedIn: 'root' })
export class NotificationService {
  private _notifications = signal<Notification[]>([]);

  // Public read-only access
  readonly notifications = this._notifications.asReadonly();
  readonly count = computed(() => this._notifications().length);
  readonly hasErrors = computed(() =>
    this._notifications().some(n => n.type === 'error')
  );

  add(message: string, type: Notification['type'] = 'info'): void {
    const notification: Notification = {
      id: crypto.randomUUID(),
      message,
      type,
    };
    this._notifications.update(current => [...current, notification]);
  }

  dismiss(id: string): void {
    this._notifications.update(current => current.filter(n => n.id !== id));
  }

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

TS
// Any component can inject and use the service
@Component({
  selector: 'app-notification-bell',
  standalone: true,
  template: `
    <button>
      Notifications ({{ notifyService.count() }})
    </button>
    @for (n of notifyService.notifications(); track n.id) {
      <div [class]="n.type">
        {{ n.message }}
        <button (click)="notifyService.dismiss(n.id)">X</button>
      </div>
    }
  `,
})
export class NotificationBellComponent {
  protected notifyService = inject(NotificationService);
}

// Another component triggers a notification
@Component({ selector: 'app-order-form' })
export class OrderFormComponent {
  private notifications = inject(NotificationService);

  submitOrder(): void {
    this.orderService.submit(this.form.value).subscribe({
      next: () => this.notifications.add('Order placed!', 'success'),
      error: () => this.notifications.add('Order failed', 'error'),
    });
  }
}
HTTP Service Pattern

Encapsulating HTTP calls in a service is a fundamental Angular pattern. The service becomes your data access layer:

TS
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';

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

export interface ProductFilters {
  category?: string;
  minPrice?: number;
  maxPrice?: number;
}

@Injectable({ providedIn: 'root' })
export class ProductService {
  private readonly apiUrl = '/api/products';

  // Cache the categories — they rarely change
  categories$ = this.http.get<string[]>(`${this.apiUrl}/categories`).pipe(
    shareReplay(1)
  );

  constructor(private http: HttpClient) {}

  getAll(filters?: ProductFilters): Observable<Product[]> {
    let params = new HttpParams();
    if (filters?.category) params = params.set('category', filters.category);
    if (filters?.minPrice) params = params.set('minPrice', String(filters.minPrice));
    if (filters?.maxPrice) params = params.set('maxPrice', String(filters.maxPrice));

    return this.http.get<Product[]>(this.apiUrl, { params });
  }

  getById(id: number): Observable<Product> {
    return this.http.get<Product>(`${this.apiUrl}/${id}`);
  }

  getFeatured(): Observable<Product[]> {
    return this.getAll().pipe(
      map(products => products.filter(p => p.price > 100).slice(0, 6))
    );
  }
}
Testing Services

TS
import { TestBed } from '@angular/core/testing';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { provideHttpClient } from '@angular/common/http';
import { UserService } from './user.service';

describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        UserService,
        provideHttpClient(),
        provideHttpClientTesting(),
      ],
    });
    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => httpMock.verify());

  it('should fetch all users', () => {
    const mockUsers = [{ id: 1, name: 'Alice', email: 'a@b.com', role: 'user' as const }];

    service.getAll().subscribe(users => {
      expect(users).toEqual(mockUsers);
    });

    const req = httpMock.expectOne('/api/users');
    expect(req.request.method).toBe('GET');
    req.flush(mockUsers);
  });
});
Best Practices
  • Use providedIn: 'root' for application-wide singletons — it enables tree-shaking (unused services are removed from the bundle)

  • Keep services focused: one service per domain concern (UserService, CartService, NotificationService)

  • Expose observable or signal properties for reactive state rather than plain mutable properties

  • Use the inject() function in Angular 14+ — it is more flexible than constructor injection

  • Never import services directly between feature modules — go through a shared service or facade

  • Write unit tests for services independently of components — they are easy to test in isolation

Warning
Avoid storing large mutable objects in a singleton service without clearing them properly. Memory from a root-scoped singleton service persists for the entire application lifetime.
Summary

Services are the backbone of Angular application architecture:

  • Decorated with @Injectable to participate in dependency injection
  • providedIn: 'root' creates a singleton — shared across the whole app
  • Can be scoped to a component or module for isolated state
  • Use services to share state, encapsulate HTTP calls, and coordinate between components
  • Inject via inject() (modern) or constructor parameters (traditional)

Next: understand Dependency Injection — the system that makes services work.