Angular HTTP Client & Services — Practical Guide
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, andswitchMap - Build HTTP interceptors for auth tokens and logging
- Leverage the
asyncpipe 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
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 theHttpClientservice when creatingDataService. 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 ofawait?” 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.
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 Method | Angular Method | Purpose |
|---|---|---|
GET | http.get<T>() | Read data |
POST | http.post<T>() | Create new data |
PUT | http.put<T>() | Replace entire resource |
PATCH | http.patch<T>() | Update part of a resource |
DELETE | http.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
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.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.
What does
switchMapdo 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.Why use the
asyncpipe instead of manual subscription? It automatically subscribes and unsubscribes, preventing memory leaks. Your component code is also simpler without subscription logic.What is the difference between
PUTandPATCH?PUTreplaces the entire resource.PATCHupdates 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
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
| Tutorial | Description |
|---|---|
| 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