HTTP Client

This section documents modern, standalone Angular — signals, the built-in @if / @for / @switch control flow, @defer, typed reactive forms, provideHttpClient, functional guards and interceptors, and server-side rendering with hydration — as described by the official documentation at angular.dev, which is the reference these pages are written and verified against.

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' });

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> }

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.

sequenceDiagram participant C as Component participant H as HttpClient participant I as Interceptor chain participant S as Server C->>H: http.get('/api/books').subscribe() H->>I: HttpRequest Note over I: authInterceptor -> loggingInterceptor -> retryInterceptor I->>S: GET /api/books (Authorization: Bearer ...) S-->>I: 200 JSON body I-->>H: HttpResponse (Book array) H-->>C: parsed body, piped through map / catchError Note over C: unsubscribe() cancels an in-flight request