HttpClient in Angular
Angular's HttpClient is the built-in service for making HTTP requests. It wraps the browser's XMLHttpRequest / fetch API, adds TypeScript generics, and returns RxJS Observables so you can compose, transform, and cancel requests with operators.
HttpClient lives in @angular/common/http and must be provided before use.
Setting Up HttpClient
In modern standalone Angular (v15+) call provideHttpClient() in your app.config.ts.
// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withFetch } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(withFetch()), // use the modern Fetch API under the hood
],
};withFetch() switches the underlying transport to the browser Fetch API. Omit it to use the legacy XMLHttpRequest transport.NgModule Setup (Older Projects)
// src/app/app.module.ts
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [HttpClientModule],
})
export class AppModule {}Making GET Requests
// src/app/services/user.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface User {
id: number;
name: string;
email: string;
}
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
private apiUrl = 'https://jsonplaceholder.typicode.com/users';
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl);
}
getUser(id: number): Observable<User> {
return this.http.get<User>(`${this.apiUrl}/${id}`);
}
}Consuming the Observable in a Component
// src/app/users/users.component.ts
import { Component, OnInit, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { UserService, User } from '../services/user.service';
import { Observable } from 'rxjs';
@Component({
selector: 'app-users',
standalone: true,
imports: [AsyncPipe],
template: `
@if (users$ | async; as users) {
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }} — {{ user.email }}</li>
}
</ul>
}
`,
})
export class UsersComponent {
private userService = inject(UserService);
users$: Observable<User[]> = this.userService.getUsers();
}async pipe automatically subscribes and unsubscribes — no need to manage subscriptions manually or call unsubscribe().POST, PUT, PATCH, DELETE
// Full CRUD service example
@Injectable({ providedIn: 'root' })
export class PostService {
private http = inject(HttpClient);
private url = 'https://jsonplaceholder.typicode.com/posts';
// POST — create
createPost(body: Partial<Post>): Observable<Post> {
return this.http.post<Post>(this.url, body);
}
// PUT — full replace
updatePost(id: number, body: Post): Observable<Post> {
return this.http.put<Post>(`${this.url}/${id}`, body);
}
// PATCH — partial update
patchPost(id: number, changes: Partial<Post>): Observable<Post> {
return this.http.patch<Post>(`${this.url}/${id}`, changes);
}
// DELETE
deletePost(id: number): Observable<void> {
return this.http.delete<void>(`${this.url}/${id}`);
}
}Request Options: Headers, Params, and Observe
import { HttpHeaders, HttpParams } from '@angular/common/http';
// Custom headers
const headers = new HttpHeaders({
'Authorization': 'Bearer my-token',
'Content-Type': 'application/json',
});
// Query parameters (?page=2&limit=20)
const params = new HttpParams()
.set('page', 2)
.set('limit', 20);
// Request with both
this.http.get<Post[]>(this.url, { headers, params }).subscribe(console.log);
// observe: 'response' — get the full HttpResponse including status/headers
this.http.get<Post[]>(this.url, { observe: 'response' }).subscribe((res) => {
console.log('Status:', res.status);
console.log('Body:', res.body);
console.log('X-Total-Count:', res.headers.get('X-Total-Count'));
});
// observe: 'events' — track upload/download progress
this.http.post(this.url, body, { observe: 'events', reportProgress: true })
.subscribe((event) => {
if (event.type === HttpEventType.UploadProgress) {
const percent = Math.round(100 * event.loaded / (event.total ?? 1));
console.log(`Upload: ${percent}%`);
}
});Error Handling
Use RxJS catchError to handle HTTP errors gracefully. The error is an HttpErrorResponse object with status, message, and error properties.
import { HttpErrorResponse } from '@angular/common/http';
import { catchError, throwError } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class DataService {
private http = inject(HttpClient);
getData(): Observable<Data[]> {
return this.http.get<Data[]>('/api/data').pipe(
catchError((error: HttpErrorResponse) => {
let message = 'An unknown error occurred';
if (error.status === 0) {
message = 'Network error — check your connection';
} else if (error.status === 401) {
message = 'Unauthorized — please log in again';
} else if (error.status === 404) {
message = 'Resource not found';
} else if (error.status >= 500) {
message = 'Server error — try again later';
}
console.error('HTTP Error:', error);
return throwError(() => new Error(message));
})
);
}
}Retry Logic
import { retry, catchError } from 'rxjs/operators';
getData(): Observable<Data[]> {
return this.http.get<Data[]>('/api/data').pipe(
retry({ count: 3, delay: 1000 }), // retry 3 times, 1 second apart
catchError(this.handleError)
);
}Cancelling Requests with takeUntil
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({ selector: 'app-search', standalone: true, template: '' })
export class SearchComponent implements OnDestroy {
private destroy$ = new Subject<void>();
private http = inject(HttpClient);
search(term: string): void {
this.http.get<Result[]>(`/api/search?q=${term}`)
.pipe(takeUntil(this.destroy$))
.subscribe((results) => {
// handle results
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}Typed HTTP with Interfaces
// Always type your HTTP calls — generics give you compile-time safety
export interface ApiResponse<T> {
data: T;
total: number;
page: number;
}
export interface Product {
id: number;
name: string;
price: number;
category: string;
}
@Injectable({ providedIn: 'root' })
export class ProductService {
private http = inject(HttpClient);
getProducts(page = 1): Observable<ApiResponse<Product[]>> {
const params = new HttpParams().set('page', page);
return this.http.get<ApiResponse<Product[]>>('/api/products', { params });
}
}File Uploads
uploadFile(file: File): Observable<any> {
const formData = new FormData();
formData.append('file', file, file.name);
return this.http.post('/api/upload', formData, {
reportProgress: true,
observe: 'events',
}).pipe(
map((event) => {
switch (event.type) {
case HttpEventType.UploadProgress:
return { progress: Math.round(100 * event.loaded / (event.total ?? 1)) };
case HttpEventType.Response:
return { done: true, body: event.body };
default:
return {};
}
})
);
}HttpClient Feature Comparison
Feature | API |
|---|---|
GET request | http.get<T>(url, options) |
POST request | http.post<T>(url, body, options) |
Custom headers | new HttpHeaders({ key: value }) |
Query params | new HttpParams().set(key, value) |
Full response | { observe: 'response' } |
Progress events | { reportProgress: true, observe: 'events' } |
Error handling | catchError((err: HttpErrorResponse) => ...) |
Retry on fail | retry({ count: N, delay: ms }) |
Best Practices
Always type HTTP calls with generics: http.get<User[]>(url)
Put HTTP logic in services, not components
Use catchError in every service method that makes network calls
Unsubscribe from long-lived subscriptions with takeUntil or takeUntilDestroyed
Use the async pipe in templates to avoid manual subscription management
Use HttpParams instead of string concatenation for query parameters
Set withFetch() in provideHttpClient() for modern fetch-based transport
Use interceptors for cross-cutting concerns like auth tokens and logging
Summary
HttpClient is the standard Angular API for all HTTP communication. Its Observable-based interface integrates naturally with RxJS operators for transformation, error handling, retry logic, and cancellation. Typed generics catch response-shape errors at compile time, and interceptors keep auth and logging concerns out of your service layer.