HTTP Client
|
This section documents modern, standalone Angular — signals, the built-in This content was generated with the assistance of AI and should be verified against angular.dev before being relied on in production. Angular ships a major release roughly every six months and its APIs continue to evolve: the examples here target the current major release; where a consulted source disagrees with the current documentation, the documentation wins and the difference is noted. This section’s bibliography lists the reference material consulted while preparing these pages. |
HttpClient is Angular’s typed wrapper over the browser’s networking APIs. It returns observables, integrates
with dependency injection, and supports a chain of interceptors. For the platform primitive it builds on, see
Browser networking.
Setup and requests
Register HttpClient once in the application config. withFetch() switches the backend from XMLHttpRequest
to the fetch API; withInterceptors([…]) installs the functional interceptor chain.
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
import { authInterceptor, loggingInterceptor } from './http/interceptors';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withFetch(),
withInterceptors([authInterceptor, loggingInterceptor]),
),
],
};
Inject HttpClient and call the verb methods. Each returns a cold Observable — the request fires on
subscribe() (the async pipe, toSignal, or an explicit call) and is cancelled on unsubscribe().
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class BookApi {
private readonly http = inject(HttpClient);
list(q: string, page: number) {
const params = new HttpParams().set('q', q).set('page', page);
return this.http.get<Book[]>('/api/books', { params }); // Observable<Book[]>
}
get(id: string) {
return this.http.get<Book>(`/api/books/${id}`);
}
create(book: NewBook) {
return this.http.post<Book>('/api/books', book);
}
replace(id: string, book: Book) {
return this.http.put<Book>(`/api/books/${id}`, book);
}
patch(id: string, changes: Partial<Book>) {
const headers = new HttpHeaders({ 'X-Reason': 'inline-edit' });
return this.http.patch<Book>(`/api/books/${id}`, changes, { headers });
}
remove(id: string) {
return this.http.delete<void>(`/api/books/${id}`);
}
}
HttpParams and HttpHeaders are immutable — every set/append returns a new instance, so chain them.
The type argument on get<T> / post<T> types the response body; Angular does not validate it at runtime.
By default the observable emits the parsed body. Change what you observe, and how the body is parsed, with the options object:
// full HttpResponse<T> (status, headers, body)
this.http.get<Book>('/api/books/1', { observe: 'response' });
// the stream of HttpEvent<T> values, including upload/download progress
this.http.post('/api/upload', form, { observe: 'events', reportProgress: true });
// non-JSON payloads
this.http.get('/report.csv', { responseType: 'text' });
this.http.get('/logo.png', { responseType: 'blob' });
See HTTP client, Setting up HttpClient, and Making requests.
Error handling
A failed request produces an HttpErrorResponse in the observable’s error channel — either a network/CORS
failure (error is a ProgressEvent, status is 0) or an HTTP error status (status is 4xx/5xx,
error is the parsed error body). Handle it with catchError.
import { catchError, retry, timer } from 'rxjs';
import { throwError } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';
list() {
return this.http.get<Book[]>('/api/books').pipe(
retry({ count: 3, delay: (_err, i) => timer(500 * 2 ** i) }), // backoff (i is 1-based): 1s, 2s, 4s
catchError((err: HttpErrorResponse) => {
const message = err.status === 0
? 'Network error -- check your connection.'
: `Server returned ${err.status}: ${err.error?.detail ?? err.message}`;
return throwError(() => new Error(message));
}),
);
}
Centralised handling (global ErrorHandler, an interceptor that catches every response, redirecting on 401)
is covered on Error handling.
Functional interceptors
An interceptor is an HttpInterceptorFn — a function that receives the outgoing request and a next handler,
and returns the response stream. It runs in the injection context, so it can inject() services. Interceptors
run in the order listed in withInterceptors([…]).
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, retry, tap, throwError, timer } from 'rxjs';
// 1. attach a bearer token to same-origin API calls
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthStore).token();
if (!token || !req.url.startsWith('/api/')) return next(req);
return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));
};
// 2. log method, url, and outcome
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
const started = performance.now();
return next(req).pipe(
tap({
next: (event) => { /* inspect HttpResponse if event.type === HttpEventType.Response */ },
error: (err: HttpErrorResponse) =>
console.error(`${req.method} ${req.url} failed (${err.status})`),
finalize: () => console.debug(`${req.method} ${req.url} ${Math.round(performance.now() - started)}ms`),
}),
);
};
// 3. retry idempotent requests with backoff
export const retryInterceptor: HttpInterceptorFn = (req, next) =>
req.method === 'GET'
? next(req).pipe(retry({ count: 2, delay: (_e, i) => timer(300 * 2 ** i) }))
: next(req);
HttpRequest is immutable; produce a modified copy with req.clone(…). See
Interceptors.
httpResource()
httpResource() wraps a GET in the signal-based resource API: pass a reactive request (a function reading
signals) and it re-fetches whenever a dependency changes, exposing value, status, error, and isLoading
as signals. It is for reading data into the view, not for mutations.
import { httpResource } from '@angular/common/http';
import { signal } from '@angular/core';
readonly bookId = signal('1');
readonly book = httpResource<Book>(() => `/api/books/${this.bookId()}`);
// template:
// @if (book.isLoading()) { <app-spinner /> }
// @else if (book.error()) { <p role="alert">Failed to load.</p> }
// @else { <h1>{{ book.value()?.title }}</h1> }
See httpResource and Signals.
Testing
provideHttpClientTesting replaces the real backend with a controllable one. HttpTestingController lets a
test assert which requests were made and flush fake responses; verify() fails the test on unexpected or
outstanding requests. Full guidance is on Testing.
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
beforeEach(() => TestBed.configureTestingModule({
providers: [provideHttpClient(), provideHttpClientTesting(), BookApi],
}));
it('requests the book list', () => {
const api = TestBed.inject(BookApi);
const http = TestBed.inject(HttpTestingController);
api.list('ng', 1).subscribe((books) => expect(books.length).toBe(1));
const req = http.expectOne('/api/books?q=ng&page=1');
expect(req.request.method).toBe('GET');
req.flush([{ id: '1', title: 'Learning Angular' }]);
http.verify();
});
Cross-origin requests
A request to a different origin is governed by the browser’s CORS policy: the server must return
Access-Control-Allow-Origin (and, for non-simple requests, respond to a preflight OPTIONS), otherwise the
browser rejects the response and HttpClient reports an HttpErrorResponse with status: 0. Angular cannot
relax this — it is enforced by the browser. See What is CORS? and, for the request/response
model underneath, Browser networking.