HttpClient provides typed request APIs, interceptors, progress events, and RxJS cancellation, but its generic types do not validate server data at runtime. Configure the client deliberately and keep transport DTOs at the application boundary.
HttpClient is Angular's typed Observable API for HTTP requests. In current Angular applications it is injectable by default; provideHttpClient remains the configuration point for features such as functional interceptors and alternative backend options.
The default backend uses fetch in current Angular, while withRequestsMadeViaParent and withInterceptors configure composition at injector boundaries and withXhr selects XMLHttpRequest when an application specifically requires it.
A request observable is cold: no request is sent until subscription, and separate subscriptions can send separate requests. Return observables from services and let the owning component, signal conversion, or template choose the subscription lifetime.
The generic type describes the response shape to TypeScript; it does not validate untrusted JSON at runtime. Validate critical API data before treating it as a domain object.
HttpClient is available for injection by default in Angular v21 and later. Add provideHttpClient to application providers when configuring features such as functional interceptors, XSRF options, JSONP, a parent injector chain, or the XHR backend.
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { authInterceptor } from './auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(withInterceptors([authInterceptor]))
]
};
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
interface User {
id: number;
name: string;
email: string;
}
@Injectable({ providedIn: 'root' })
export class DataService {
private http = inject(HttpClient);
getUsers(): Observable<User[]> {
return this.http.get<User[]>('/api/users');
}
}
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { DataService } from './data.service';
@Component({
selector: 'app-users',
standalone: true,
template: `
@for (user of users(); track user.id) {
<p>{{ user.name }} - {{ user.email }}</p>
}
`
})
export class UsersComponent {
private dataService = inject(DataService);
users = toSignal(this.dataService.getUsers(), { initialValue: [] });
}
All HTTP methods return an Observable. You must subscribe or use toSignal() to get the data.
Use HttpParams and HttpHeaders because request objects are immutable; assignment methods return new instances. Set observe to response when status or headers matter and responseType when the payload is text, blob, or array buffer.
GET should be safe and cacheable according to the API contract. Retry idempotent operations cautiously; do not retry a payment or other write unless the server supports an idempotency key or equivalent contract.
Unsubscribing cancels an in-flight request when supported. switchMap is useful for typeahead or route-driven requests because it unsubscribes the obsolete inner request when a newer value arrives.
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
const BASE = 'https://jsonplaceholder.typicode.com';
@Injectable({ providedIn: 'root' })
export class ApiService {
private http = inject(HttpClient);
// GET
getPost(id: number) {
return this.http.get(`${BASE}/posts/${id}`);
}
// POST
createPost(data: object) {
return this.http.post(`${BASE}/posts`, data);
}
// PUT
updatePost(id: number, data: object) {
return this.http.put(`${BASE}/posts/${id}`, data);
}
// PATCH
patchPost(id: number, data: object) {
return this.http.patch(`${BASE}/posts/${id}`, data);
}
// DELETE
deletePost(id: number) {
return this.http.delete(`${BASE}/posts/${id}`, { responseType: 'text' });
}
}
Use params for URL query data and headers for request metadata. Because both classes are immutable, keep the value returned by set, append, or delete. Pass observe: "response" when status and response headers are part of the feature contract.
Set responseType to text, blob, or arraybuffer for non-JSON payloads. When options are extracted into a variable, preserve literal types with as const so TypeScript selects the correct HttpClient overload.
Progress events require observe: "events" and reportProgress: true. The Fetch backend does not report upload progress, so an upload UI that depends on it needs the XHR backend and explicit browser testing.
search(term: string, page: number) {
const params = new HttpParams()
.set('q', term)
.set('page', page);
return this.http.get<readonly ProductDto[]>('/api/products', {
params,
observe: 'response' as const
}).pipe(
map(response => ({
products: response.body ?? [],
total: Number(response.headers.get('X-Total-Count') ?? 0)
}))
);
}
upload(file: File) {
const body = new FormData();
body.append('file', file);
return this.http.post('/api/uploads', body, {
observe: 'events',
reportProgress: true
}).pipe(
map(event => event.type === HttpEventType.UploadProgress
? Math.round(100 * event.loaded / (event.total ?? event.loaded))
: null)
);
}
HttpClient reports backend status failures and network failures as HttpErrorResponse. A status of 0 commonly indicates a network, CORS, or client-side failure; a nonzero status came from an HTTP response. Preserve that distinction when mapping transport errors to application states.
Use the request timeout option for a transport deadline and RxJS timeout when a broader stream contract needs it. Retrying is safe only when the operation is idempotent or the server honors an idempotency key.
Unsubscribing cancels the active request. switchMap gives newer route parameters or search terms ownership by unsubscribing the previous request; takeUntilDestroyed binds a manual subscription to its Angular owner.
readonly results$ = this.searchControl.valueChanges.pipe(
map(value => value.trim()),
debounceTime(250),
distinctUntilChanged(),
switchMap(term => this.http.get<readonly Product[]>('/api/products', {
params: { q: term },
timeout: 5000
}).pipe(
catchError(error => of({ error, products: [] as readonly Product[] }))
))
);
Register provideHttpClient before provideHttpClientTesting, call the service method, then capture the request with HttpTestingController. Assert method, URL, parameters, headers, and body before flushing a controlled response.
Use flush with a status and statusText for backend failures and ProgressEvent for a network-level error. Call verify after each test so an unexpected duplicate or unflushed request fails the suite.
TestBed.configureTestingModule({
providers: [
ProductService,
provideHttpClient(),
provideHttpClientTesting()
]
});
const service = TestBed.inject(ProductService);
const http = TestBed.inject(HttpTestingController);
service.getProduct(7).subscribe(product =>
expect(product.name).toBe('Keyboard')
);
const request = http.expectOne('/api/products/7');
expect(request.request.method).toBe('GET');
request.flush({ id: 7, name: 'Keyboard' });
http.verify();
Prefer functional interceptors and register them with withInterceptors in provideHttpClient. Class-based interceptors remain available through the compatibility configuration, but functional ordering is more predictable.
import { HttpInterceptorFn } from '@angular/common/http';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = localStorage.getItem('token');
if (token) {
const authReq = req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
});
return next(authReq);
}
return next(req);
};
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withInterceptors([authInterceptor]))
]
};
A subscription. HttpClient returns a cold Observable, so calling the method only builds the request.
The generic type is a compile-time assertion for TypeScript; HttpClient does not validate the server response at runtime. If the API returns { data: [...] } or malformed user objects, the code can compile and still fail when the template reads user.id or user.email.
The Fetch backend is useful for modern browser behavior and server rendering, but it is not identical to XMLHttpRequest. Applications that depend on upload progress events, certain legacy interceptor assumptions, or browser behaviors specific to XHR need testing before switching.
Explore 500+ free tutorials across 20+ languages and frameworks.