Skip to content
Angular HTTP Client & Services — Practical Guide

Angular HTTP Client & Services — Practical Guide

DodaTech Updated Jun 6, 2026 11 min read

Angular HTTP Client and services let your app communicate with backend servers. This practical guide covers creating injectable services, performing CRUD operations, handling errors, and using interceptors for authentication tokens.

What You’ll Learn

  • Create injectable services with @Injectable
  • Perform CRUD operations (GET, POST, PUT, PATCH, DELETE) using HttpClient
  • Handle HTTP errors gracefully with catchError
  • Use RxJS operators like retry, debounceTime, and switchMap
  • Build HTTP interceptors for auth tokens and logging
  • Leverage the async pipe for automatic subscription management

Why HTTP Services Matter

Every real-world app communicates with a server. When Durga Antivirus Pro scans a file, it sends the scan request to a backend API and displays results. When Doda Browser syncs bookmarks, it uses HTTP requests. Services are Angular’s way of organizing this logic into reusable, testable units that any component can use without duplicating code.

    flowchart LR
    A["Angular Basics & Routing"] --> B["**Angular HTTP & Services**"]
    B --> C["Angular Advanced"]
    style B fill:#f97316,stroke:#c2410c,color:#fff
  
Prerequisites: Angular component basics, https://tutorials.dodatech.com/frameworks/angular/angular-routing/ knowledge, and familiarity with REST API and JSON.

What Is a Service?

A service is a class that encapsulates reusable logic — typically data access, business logic, or shared state. Think of a service as a waiter in a restaurant: components (customers) tell the service what they need, and the service brings it from the kitchen (the server/API). Components don’t need to know how the kitchen works.

    flowchart LR
    A[Component] -->|calls method| B[Service]
    B -->|HttpClient| C[REST API]
    B -->|returns Observable| A
    C -->|JSON response| B
    D[Interceptor] -->|modifies request/response| B
  

Creating Your First Service

// data.service.ts
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";

@Injectable({
  providedIn: "root"    // Singleton — one instance for the whole app
})
export class DataService {
  private apiUrl = "https://jsonplaceholder.typicode.com";

  // HttpClient is injected via the constructor
  constructor(private http: HttpClient) {}

  getPosts(): Observable<any[]> {
    return this.http.get<any[]>(`${this.apiUrl}/posts`);
  }

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

Let’s break down each piece:

  • @Injectable({ providedIn: "root" }) — This decorator marks the class as available for dependency injection. providedIn: "root" means Angular creates a single instance (singleton) for the entire app.
  • constructor(private http: HttpClient) {} — Angular automatically injects the HttpClient service when creating DataService. This is Dependency Injection in action.
  • this.http.get<any[]>(url) — Makes an HTTP GET request. The generic type <any[]> tells TypeScript what shape the response data has.
  • Observable<any[]> — HTTP methods return Observables, not Promises. Observables are lazy — they don’t start the request until something subscribes.

Using a Service in a Component

import { Component, OnInit } from "@angular/core";
import { CommonModule } from "@angular/common";
import { DataService } from "./data.service";

@Component({
  selector: "app-posts",
  standalone: true,
  imports: [CommonModule],
  template: `
    @if (loading) { <p>Loading...</p> }
    @if (error) { <p class="error">{{ error }}</p> }
    @for (post of posts; track post.id) {
      <div class="post">
        <h3>{{ post.title }}</h3>
        <p>{{ post.body }}</p>
      </div>
    }
  `
})
export class PostsComponent implements OnInit {
  posts: any[] = [];
  loading = true;
  error: string | null = null;

  constructor(private dataService: DataService) {}

  ngOnInit() {
    this.dataService.getPosts().subscribe({
      next: (data) => {         // Success handler
        this.posts = data;
        this.loading = false;
      },
      error: (err) => {         // Error handler
        this.error = "Failed to load posts";
        this.loading = false;
        console.error(err);
      }
    });
  }
}

You might be wondering: “Why subscribe() instead of await?” Observables are designed for streams of data (multiple values over time). Even for single HTTP requests, using Observables gives you composability with RxJS operators — like retrying on failure or cancelling in-flight requests.

Always handle the error callback in subscriptions. Unhandled errors can crash the entire application — and users won’t know what went wrong.

Dependency Injection — How It Works

Angular’s DI system is like a registry. You declare “I need an HttpClient” and Angular provides it:

// Service with dependencies on other services
@Injectable({ providedIn: "root" })
export class AuthService {
  constructor(private http: HttpClient, private router: Router) {}

  login(email: string, password: string): Observable<any> {
    return this.http.post("/api/auth/login", { email, password });
  }

  logout() {
    localStorage.removeItem("token");
    this.router.navigate(["/login"]);
  }
}

Angular sees the constructor parameters, looks up the corresponding providers, and hands you the instances. You never call new HttpClient() — Angular manages the lifecycle.

Injection Tokens — For Non-Class Dependencies

Sometimes you need to inject a value, not a class. Use InjectionToken:

import { InjectionToken } from "@angular/core";

export const API_URL = new InjectionToken<string>("API_URL");

// Provide in app.config.ts
providers: [
  { provide: API_URL, useValue: "https://api.example.com" }
];

// Inject in service
constructor(@Inject(API_URL) private apiUrl: string) {}

HttpClient CRUD Operations

Here’s a complete service for a Product API:

// api.service.ts
import { Injectable } from "@angular/core";
import { HttpClient, HttpParams, HttpHeaders } from "@angular/common/http";
import { Observable } from "rxjs";

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

@Injectable({ providedIn: "root" })
export class ApiService {
  private baseUrl = "/api/products";

  constructor(private http: HttpClient) {}

  // GET — Read all
  getProducts(): Observable<Product[]> {
    return this.http.get<Product[]>(this.baseUrl);
  }

  // GET — Read one
  getProduct(id: number): Observable<Product> {
    return this.http.get<Product>(`${this.baseUrl}/${id}`);
  }

  // POST — Create
  createProduct(product: Product): Observable<Product> {
    return this.http.post<Product>(this.baseUrl, product);
  }

  // PUT — Full update
  updateProduct(id: number, product: Product): Observable<Product> {
    return this.http.put<Product>(`${this.baseUrl}/${id}`, product);
  }

  // PATCH — Partial update
  patchProduct(id: number, changes: Partial<Product>): Observable<Product> {
    return this.http.patch<Product>(`${this.baseUrl}/${id}`, changes);
  }

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

  // GET with query parameters
  searchProducts(query: string): Observable<Product[]> {
    const params = new HttpParams().set("q", query);
    return this.http.get<Product[]>(this.baseUrl, { params });
  }

  // GET with custom headers
  getWithToken(): Observable<Product[]> {
    const headers = new HttpHeaders().set(
      "Authorization", "Bearer " + localStorage.getItem("token")
    );
    return this.http.get<Product[]>(this.baseUrl, { headers });
  }
}
HTTP MethodAngular MethodPurpose
GEThttp.get<T>()Read data
POSThttp.post<T>()Create new data
PUThttp.put<T>()Replace entire resource
PATCHhttp.patch<T>()Update part of a resource
DELETEhttp.delete<T>()Remove a resource

RxJS Operators for HTTP

Observables shine when you need to compose, transform, or retry requests:

import { catchError, map, debounceTime, distinctUntilChanged, switchMap, retry } from "rxjs/operators";
import { of, throwError } from "rxjs";

// Error handling with automatic retry
getProducts(): Observable<Product[]> {
  return this.http.get<Product[]>(this.baseUrl).pipe(
    retry(2),                                     // Retry twice on failure
    catchError(this.handleError<Product[]>("getProducts", []))
  );
}

private handleError<T>(operation = "operation", result?: T) {
  return (error: any): Observable<T> => {
    console.error(`${operation} failed:`, error.message);
    return of(result as T);                       // Return safe default
  };
}

// Live search with debounce
searchProducts(terms: Observable<string>): Observable<Product[]> {
  return terms.pipe(
    debounceTime(300),                            // Wait 300ms after last keystroke
    distinctUntilChanged(),                       // Don't repeat same search
    switchMap(term => this.http.get<Product[]>(`${this.baseUrl}?q=${term}`))
  );
}
  • retry(2) — Automatically retries the request up to 2 times if it fails.
  • catchError() — Intercepts errors and returns a safe default value instead of crashing.
  • debounceTime(300) — Only fires the search after the user stops typing for 300ms.
  • switchMap() — Cancels the previous request if a new one comes in (important for search).
  • distinctUntilChanged() — Skips duplicate values (same search term twice).

HTTP Interceptors

Interceptors are functions that sit between your app and the server. They can modify every outgoing request or incoming response:

// auth.interceptor.ts — Add token to every request
import { HttpInterceptorFn } from "@angular/common/http";

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = localStorage.getItem("token");

  // Clone the request and add the Authorization header
  const cloned = token
    ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
    : req;

  return next(cloned);  // Pass to the next handler
};
// logging.interceptor.ts — Log every request/response
import { HttpInterceptorFn, HttpEvent, HttpEventType } from "@angular/common/http";
import { tap } from "rxjs/operators";

export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
  console.log(`➡️ ${req.method} ${req.url}`);

  return next(req).pipe(
    tap((event: HttpEvent<any>) => {
      if (event.type === HttpEventType.Response) {
        console.log(`⬅️ ${req.method} ${req.url}${event.status}`);
      }
    })
  );
};
// error.interceptor.ts — Global error handling
import { HttpInterceptorFn, HttpErrorResponse } from "@angular/common/http";
import { inject } from "@angular/core";
import { Router } from "@angular/router";
import { catchError, throwError } from "rxjs";

export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  const router = inject(Router);

  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 401) {
        // Unauthorized — redirect to login
        localStorage.removeItem("token");
        router.navigate(["/login"]);
      }
      if (error.status === 0) {
        // Network error — server is unreachable
        console.error("Network error — is the server running?");
      }
      return throwError(() => error);
    })
  );
};

Registering Interceptors

// app.config.ts
import { provideHttpClient, withInterceptors } from "@angular/common/http";

providers: [
  provideHttpClient(
    withInterceptors([authInterceptor, loggingInterceptor, errorInterceptor])
  )
]

Interceptors run in the order you list them. The auth interceptor adds the token, then the logging interceptor logs the request.

The Async Pipe — Automatic Subscriptions

Instead of manually subscribing in your component, use the async pipe in templates:

@if (posts$ | async; as posts) {
  @for (post of posts; track post.id) {
    <p>{{ post.title }}</p>
  }
} @else {
  <p>Loading...</p>
}
import { Observable } from "rxjs";

export class PostsComponent {
  posts$: Observable<any[]> = this.dataService.getPosts();
  constructor(private dataService: DataService) {}
}

The async pipe subscribes to the Observable and returns the latest value. It automatically unsubscribes when the component is destroyed — no memory leaks, no manual cleanup.

Common Mistakes

1. Not unsubscribing from Observables

Every subscription in a component that isn’t cleaned up causes a memory leak. Use the async pipe, takeUntil, or manually unsubscribe in ngOnDestroy.

2. Nesting subscribe calls

// ❌ Bad — nested subscriptions
this.userService.getUser().subscribe(user => {
  this.ordersService.getOrders(user.id).subscribe(orders => {
    this.orders = orders;
  });
});

// ✅ Good — use switchMap
this.userService.getUser().pipe(
  switchMap(user => this.ordersService.getOrders(user.id))
).subscribe(orders => this.orders = orders);

3. Forgetting to provide HttpClient

Every provideHttpClient() call in app.config.ts is required. Without it, injecting HttpClient throws an error.

4. Not handling HTTP errors

Always include catchError in service methods or a global error interceptor. Unhandled errors crash the application silently.

5. Mutating service state directly from components

Services should encapsulate state. Expose methods like addItem() and removeItem() instead of allowing direct mutation of shared arrays.

6. Making HTTP calls in the constructor

The constructor runs before @Input() properties are set. Use ngOnInit for data fetching.

Practice Questions

  1. What does providedIn: "root" do? It makes the service a singleton available across the entire application. Angular creates one instance and injects it wherever needed.

  2. How is an HTTP interceptor different from a service method? An interceptor runs globally for every HTTP request/response. A service method handles specific API endpoints. Interceptors are for cross-cutting concerns like auth tokens and logging.

  3. What does switchMap do in the context of HTTP requests? It cancels the previous HTTP request when a new one arrives. This is essential for search-as-you-type features to prevent stale results.

  4. Why use the async pipe instead of manual subscription? It automatically subscribes and unsubscribes, preventing memory leaks. Your component code is also simpler without subscription logic.

  5. What is the difference between PUT and PATCH? PUT replaces the entire resource. PATCH updates only the specified fields.

Challenge

Build a task manager service with full CRUD operations. Create a component that displays tasks, allows adding new tasks, toggles completion status, and deletes tasks. Use the async pipe and handle errors gracefully.

FAQ

What is a service in Angular?
A class that encapsulates reusable logic or data access. Services are singletons by default when provided in root, injected into components via dependency injection.
How does dependency injection work?
Angular’s DI system creates and manages service instances. When a component declares a constructor parameter with a service type, Angular provides the appropriate instance automatically.
What is an HTTP interceptor?
A function that intercepts outgoing HTTP requests or incoming responses. Common uses: adding auth headers, logging, error handling, caching.
How do I handle errors from HttpClient?
Use catchError operator in service methods, or create a global error interceptor. For user-facing errors, display messages in the component.
What is the async pipe?
A pipe (| async) that subscribes to an Observable in the template and returns the latest value. It automatically unsubscribes when the component is destroyed.
How do I share data between components?
Use a service with shared state or signals. Services are the simplest approach — inject the same service in multiple components to share data.

Try It Yourself

Create a service that fetches data from https://jsonplaceholder.typicode.com/users. Build a component that displays users in a card layout with loading and error states. Then add a search input that filters users by name using debounceTime and switchMap.

// user.service.ts — starter
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";

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

@Injectable({ providedIn: "root" })
export class UserService {
  private apiUrl = "https://jsonplaceholder.typicode.com/users";
  constructor(private http: HttpClient) {}

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

What’s Next

TutorialDescription
https://tutorials.dodatech.com/frameworks/angular/angular-advanced/Signals, change detection, dynamic components, SSR
https://tutorials.dodatech.com/frameworks/angular/angular-forms/Submit form data to APIs with reactive forms
https://tutorials.dodatech.com/frameworks/angular/reference/Quick Angular API reference and cheatsheet

Related topics: REST API, RxJS (Observables), JSON.

What’s Next

Congratulations on completing this Angular Http tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro