Services and @Injectable — Creating Singleton Services

Angular services are classes that provide shared functionality across the application — HTTP communication, state management, utilities, and business logic that does not belong in UI components. @Injectable({ providedIn: 'root' }) registers the service as a singleton in the root injector — one instance for the entire application. When multiple components inject the same root service, they all share the same instance and see the same state. This makes services the natural home for shared state and API communication in Angular.

Creating and Using Services

import { Injectable, inject, signal, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '@env/environment';
import { PostSummaryDto, PostDto, PagedResult } from '@models/post';

// ── PostsService — HTTP + local state ─────────────────────────────────────
@Injectable({ providedIn: 'root' })  // singleton — one instance app-wide
export class PostsService {
  private http    = inject(HttpClient);
  private baseUrl = environment.apiBaseUrl;

  // ── Signal-based local state ──────────────────────────────────────────────
  private _posts   = signal<PostSummaryDto[]>([]);
  private _total   = signal(0);
  private _loading = signal(false);
  private _error   = signal<string | null>(null);

  // ── Public read-only computed signals ─────────────────────────────────────
  readonly posts    = this._posts.asReadonly();
  readonly total    = this._total.asReadonly();
  readonly loading  = this._loading.asReadonly();
  readonly error    = this._error.asReadonly();
  readonly isEmpty  = computed(() => this._posts().length === 0 && !this._loading());

  // ── HTTP methods ──────────────────────────────────────────────────────────
  loadPublished(page = 1, size = 10): void {
    this._loading.set(true);
    this._error.set(null);

    this.http.get<PagedResult<PostSummaryDto>>(
      `${this.baseUrl}/api/posts?page=${page}&size=${size}`
    ).subscribe({
      next:  result => {
        this._posts.set(result.items);
        this._total.set(result.total);
        this._loading.set(false);
      },
      error: err => {
        this._error.set('Failed to load posts. Please try again.');
        this._loading.set(false);
      },
    });
  }

  getBySlug(slug: string) {
    return this.http.get<PostDto>(`${this.baseUrl}/api/posts/by-slug/${slug}`);
  }
}

// ── Component using the service ───────────────────────────────────────────
@Component({
  selector:   'app-post-list',
  standalone:  true,
  template: `
    @if (postsService.loading()) {
      <app-spinner />
    } @else if (postsService.error()) {
      <p class="error">{{ postsService.error() }}</p>
    } @else {
      @for (post of postsService.posts(); track post.id) {
        <app-post-card [post]="post" />
      } @empty {
        <p>No posts found.</p>
      }
    }
  `,
})
export class PostListComponent implements OnInit {
  protected postsService = inject(PostsService);  // protected: accessible in template

  ngOnInit() {
    this.postsService.loadPublished();
  }
}
Note: providedIn: 'root' registers the service in the root injector — it is created lazily (on first injection) and lives for the lifetime of the application. This is the recommended default for most services. The alternative, listing the service in a component’s providers: [] array, creates a new service instance for each component instance — useful for component-scoped state but wrong for shared state. Never list providedIn: 'root' services in component providers — that creates a second instance, breaking shared state.
Tip: Expose service state as readonly computed signals to prevent external mutation. private _posts = signal([]); with readonly posts = this._posts.asReadonly() means components can read postsService.posts() but cannot call postsService._posts.set(). This enforces the service as the single point of mutation — components dispatch actions to the service, the service updates state, components observe the results. This is the unidirectional data flow pattern applied to service-based state management.
Warning: Services with providedIn: 'root' persist for the application’s lifetime — they are never destroyed. This means any state they hold persists across route navigations. If a PostsService caches a list of posts, navigating away and back shows the cached list (which may be stale). Design root services to either clear state on demand (expose a reset() method) or refresh data on each load call. Services that must be fresh on every component creation belong in component-level providers, not the root.

Common Mistakes

Mistake 1 — Putting business logic in components instead of services (untestable, non-reusable)

❌ Wrong — HTTP calls and state management in the component class; second component that needs the same data duplicates the code.

✅ Correct — all HTTP, state, and business logic in services; components inject services and bind to their signals.

Mistake 2 — Making service state directly mutable from components

❌ Wrong — public posts = signal<PostDto[]>([]); any component can call postsService.posts.set([]).

✅ Correct — private _posts = signal([]); readonly posts = this._posts.asReadonly().

🧠 Test Yourself

Two components both inject PostsService with providedIn: 'root'. Component A calls postsService.loadPublished(). Does Component B’s template update?