| |

Angular 35 ๐Ÿ…ฐ๏ธ HTTP Client โ€” Talking to APIs

Most front-end applications exist to communicate with a server. Fetching user data, submitting forms, loading configuration, uploading files โ€” these are the operations that turn a static interface into a working application. Angular provides HttpClient for this purpose, a service built on the browser’s native fetch API (with XMLHttpRequest as an option) that wraps HTTP calls in RxJS Observables and integrates them with Angular’s dependency injection, interceptors, and testing utilities . The key characteristics are: requests are cold Observables that only fire when subscribed to, the generic type parameter is a type assertion rather than runtime validation, and the entire request and response flow can be intercepted and transformed globally . This chapter covers the setup, the request methods, error handling, headers and parameters, response type options, and the interceptor pattern that makes concerns like authentication and logging reusable across every request.

Key point: HttpClient is provided through provideHttpClient() and injected wherever needed. Its methods (get, post, put, patch, delete) return cold Observables โ€” no request is sent until subscribe() is called . The generic type parameter is a compile-time assertion; the actual response is not validated against it . Interceptors are functions that receive the outgoing request and a next handler, and can modify the request, transform the response, or handle errors before forwarding to the next interceptor in the chain .


Setting up HttpClient

HttpClient is provided through the provideHttpClient function, which is added to the application’s providers array. In Angular v21 and later, it is available for injection by default, so provideHttpClient() with no arguments is only needed when you want to configure features .

import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [provideHttpClient()],
};

Once provided, HttpClient is injected like any other service using inject() or constructor injection .

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({ providedIn: 'root' })
export class UserService {
  private readonly http = inject(HttpClient);
}

Why the service pattern is recommended. The Angular team explicitly recommends encapsulating backend calls in injectable services rather than components . A service centralizes the API logic, makes it reusable across components, and keeps the component focused on presentation. The service also provides a natural place for transforming responses, handling errors, and caching.

Why provideHttpClient instead of HttpClientModule. The older HttpClientModule is still supported for NgModule-based applications, but provideHttpClient is preferred for standalone applications and offers more stable behavior in multi-injector configurations . The HttpClientModule sets up withInterceptorsFromDi() and withXhr() by default, which are not the modern defaults.

Why the fetch backend is the default. Angular uses the browser’s fetch API by default, which is more modern and available in environments where XMLHttpRequest is not supported . The withXhr() feature switches back to XMLHttpRequest when upload progress events are required, because fetch does not produce them. The withXhr() option should not be used in server-side rendering environments because XHR support on the server is deprecated .

Why the Observable is cold. A cold Observable does not start producing values until it is subscribed to. This means this.http.get('/api/users') creates a description of a request but does not send it. Calling .subscribe() sends it. This is why mutation requests โ€” post, put, patch, delete โ€” must be subscribed to actually execute . The benefit of cold Observables is that they can be piped, transformed, retried, and composed before any network activity occurs.


Making requests

The HttpClient service has a method for each HTTP verb. Each returns an Observable of the response body, typed by the generic parameter .

// GET โ€” retrieve data
this.http.get<User[]>('/api/users').subscribe(users => {
  console.log(users);
});

// POST โ€” create a resource
this.http.post<User>('/api/users', newUser).subscribe(created => {
  console.log(created);
});

// PUT โ€” replace a resource
this.http.put<User>(`/api/users/${id}`, updatedUser).subscribe(user => {
  console.log(user);
});

// DELETE โ€” remove a resource
this.http.delete<void>(`/api/users/${id}`).subscribe(() => {
  console.log('Deleted');
});

The generic type parameter โ€” User, User[], void โ€” describes the expected response body. It does not validate the response at runtime; if the server returns something else, TypeScript will not catch it . For untrusted data, the unknown type is a safer choice, forcing the caller to validate before using the value.

Why the body argument comes before options. For post, put, and patch, the method signature is (url, body, options). The body is serialized automatically based on its type: a plain object becomes JSON, a FormData instance is sent as multipart form data, and HttpParams is sent as application/x-www-form-urlencoded . This means most calls do not need to manually set the Content-Type header.

Why subscription is required for mutations. Because the Observables are cold, a post call without .subscribe() never fires. This is a common mistake for developers coming from fetch or axios, where the request fires immediately. The fix is to subscribe, or to use the async pipe in a template, or to convert to a Promise with firstValueFrom .

Why async pipe is preferred in templates. Subscribing manually in a component requires unsubscribing in ngOnDestroy to prevent memory leaks. The async pipe handles subscription and unsubscription automatically, and it works with HttpClient Observables without any changes .

@Component({
  template: `
    @if (users$ | async; as users) {
      @for (user of users; track user.id) {
        <div>{{ user.name }}</div>
      }
    }
  `,
})
export class UserListComponent {
  private readonly userService = inject(UserService);
  users$ = this.userService.getUsers();
}

Headers, parameters, and response types

Requests can be customized through the options object. The most common options are headers, params, responseType, and observe .

this.http.get<Config>('/api/config', {
  headers: { 'X-Debug-Level': 'verbose' },
  params: { filter: 'all', page: '1' },
  responseType: 'json',
  observe: 'response',
}).subscribe(response => {
  console.log('Status:', response.status);
  console.log('Body:', response.body);
});

Why HttpHeaders and HttpParams are immutable. Both classes use immutable data structures. Calling .set() or .append() returns a new instance rather than modifying the original. This is why chaining works: new HttpHeaders().set('A', '1').set('B', '2') produces a new instance with both headers . Mutating methods that return void do not exist; the returned instance must be used.

Why responseType needs a literal value. The responseType option affects the TypeScript return type of the method. If it is extracted into a variable typed as string, the compiler cannot narrow the return type correctly. The fix is to use as const: responseType: 'text' as const .

Why observe: 'response' is useful. By default, HttpClient returns the response body only. Setting observe: 'response' returns the full HttpResponse object, which includes status, headers, statusText, and url in addition to body. This is useful for reading response headers like pagination links or rate-limit information .

Response type values:

responseTypeReturned typeUse case
'json' (default)Typed bodyAPIs returning JSON
'text'stringPlain text, CSV
'blob'BlobImages, file downloads
'arraybuffer'ArrayBufferBinary data, raw bytes

Error handling

HttpClient reports failures through the HttpErrorResponse class, which is delivered as an error notification on the Observable . The status property distinguishes network failures from server failures: status 0 indicates a network or CORS error, while status codes 4xx and 5xx come from the server.

import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';

getUser(id: string): Observable<User> {
  return this.http.get<User>(`/api/users/${id}`).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 0) {
        console.error('Network error:', error.message);
      } else if (error.status === 404) {
        console.error('User not found');
      } else {
        console.error(`Server error: ${error.status}`);
      }
      return throwError(() => new Error('Failed to load user'));
    }),
  );
}

The catchError operator intercepts the error, logs it, and returns a new Observable via throwError. Returning throwError keeps the error chain going โ€” the subscriber’s error callback still fires . If catchError returned of(fallbackValue), the error would be swallowed and the subscriber would receive the fallback as a success.

Why HttpErrorResponse is the type to catch. The error object passed to catchError is an HttpErrorResponse instance. It has status, statusText, error (the response body, often a validation error object), message, url, and headers. Inspecting error.status distinguishes the failure modes .

Why status 0 is special. A status of 0 means the request never reached the server, or the response never came back. Common causes are network disconnection, CORS preflight failure, or a timeout. The error.error property is typically a ProgressEvent rather than a server response body .

Why retry logic belongs in an interceptor. Retrying failed requests with exponential backoff is a common pattern, and it is best implemented once as an interceptor rather than repeated in every service method . RxJS provides the retry and retryWhen operators for this, and placing them in an interceptor ensures every request benefits without service code changes.


Interceptors

Interceptors are middleware functions that process every request made through HttpClient. They receive the outgoing request and a next handler, and return an Observable of the response. They can modify the request, transform the response, handle errors, or short-circuit the chain entirely .

import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthService).getToken();

  if (token) {
    const cloned = req.clone({
      headers: req.headers.set('Authorization', `Bearer ${token}`),
    });
    return next(cloned);
  }

  return next(req);
};

The interceptor is a plain function with the signature (req: HttpRequest<unknown>, next: HttpHandlerFn) => Observable<HttpEvent<unknown>> . It injects dependencies using inject() because functional interceptors run in an injection context. The req.clone() method is required because HttpRequest instances are immutable โ€” the original cannot be modified .

Registering interceptors:

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([authInterceptor, loggingInterceptor, errorInterceptor]),
    ),
  ],
};

The withInterceptors feature takes an array of interceptor functions. They run in the order listed: authInterceptor processes the request first, then forwards to loggingInterceptor, which forwards to errorInterceptor, which forwards to the backend .

Why next must always be called. Every interceptor must return the result of calling next() with either the original request or a cloned version. If an interceptor returns without calling next(), the request chain is broken and the request never reaches the server. The only exception is an interceptor that deliberately short-circuits โ€” such as a cache interceptor returning a cached response without making a network call .

Why interceptors are the right place for cross-cutting concerns. Authentication tokens, loading spinners, logging, caching, retry logic, and request prefixing are concerns that apply to every request or a class of requests. Implementing them in each service method would duplicate code and create inconsistency. An interceptor runs once and applies uniformly .

Common interceptor patterns:

PatternPurpose
AuthAdd Authorization header
LoggingLog request URL, method, duration
Loading indicatorShow spinner during requests
CachingReturn cached response for GET
API prefixPrepend base URL to relative paths
Error handlingGlobal error toast or redirect

Why functional interceptors are preferred. Angular recommends functional interceptors over the older class-based HttpInterceptor implementation because they have more predictable ordering and work naturally with the injection context . Class-based interceptors are still supported through withInterceptorsFromDi() for existing code, but new code should use the functional form.


Complete Example Session

import { Component, inject, signal } from '@angular/core';
import { HttpClient, HttpErrorResponse, provideHttpClient, withInterceptors } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { catchError, throwError, tap, finalize } from 'rxjs/operators';
import { Observable, of } from 'rxjs';

// ============================================
// PART 1: AUTH INTERCEPTOR
// ============================================

import { HttpInterceptorFn } from '@angular/common/http';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = localStorage.getItem('token');
  if (token) {
    return next(req.clone({
      setHeaders: { Authorization: `Bearer ${token}` },
    }));
  }
  return next(req);
};

// ============================================
// PART 2: LOGGING INTERCEPTOR
// ============================================

export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
  const started = Date.now();
  return next(req).pipe(
    tap({
      next: (event) => {
        if (event.type === 4 /* HttpEventType.Response */) {
          console.log(`${req.method} ${req.url} โ€” ${Date.now() - started}ms`);
        }
      },
      error: (err) => {
        console.error(`${req.method} ${req.url} โ€” failed`, err);
      },
    }),
  );
};

// ============================================
// PART 3: SERVICE WITH ERROR HANDLING
// ============================================

export interface User {
  id: string;
  name: string;
  email: string;
}

@Injectable({ providedIn: 'root' })
export class UserService {
  private readonly http = inject(HttpClient);
  private readonly baseUrl = '/api/users';

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.baseUrl).pipe(
      catchError(this.handleError),
    );
  }

  getUser(id: string): Observable<User> {
    return this.http.get<User>(`${this.baseUrl}/${id}`).pipe(
      catchError(this.handleError),
    );
  }

  createUser(user: Omit<User, 'id'>): Observable<User> {
    return this.http.post<User>(this.baseUrl, user).pipe(
      catchError(this.handleError),
    );
  }

  deleteUser(id: string): Observable<void> {
    return this.http.delete<void>(`${this.baseUrl}/${id}`).pipe(
      catchError(this.handleError),
    );
  }

  private handleError(error: HttpErrorResponse) {
    let message = 'An unknown error occurred';
    if (error.status === 0) {
      message = 'Network error โ€” check your connection';
    } else if (error.status === 404) {
      message = 'Resource not found';
    } else if (error.status >= 500) {
      message = 'Server error โ€” try again later';
    }
    console.error(message, error);
    return throwError(() => new Error(message));
  }
}

// ============================================
// PART 4: COMPONENT USING THE SERVICE
// ============================================

@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [],
  template: `
    @if (loading()) {
      <p>Loading...</p>
    } @else if (error()) {
      <p>Error: {{ error() }}</p>
      <button (click)="load()">Retry</button>
    } @else {
      @for (user of users(); track user.id) {
        <div>
          {{ user.name }} โ€” {{ user.email }}
          <button (click)="remove(user.id)">Delete</button>
        </div>
      }
    }
  `,
})
export class UserListComponent {
  private readonly userService = inject(UserService);

  readonly users = signal<User[]>([]);
  readonly loading = signal(false);
  readonly error = signal<string | null>(null);

  load(): void {
    this.loading.set(true);
    this.error.set(null);
    this.userService.getUsers().subscribe({
      next: (users) => {
        this.users.set(users);
        this.loading.set(false);
      },
      error: (err) => {
        this.error.set(err.message);
        this.loading.set(false);
      },
    });
  }

  remove(id: string): void {
    this.userService.deleteUser(id).subscribe({
      next: () => {
        this.users.update((list) => list.filter((u) => u.id !== id));
      },
    });
  }
}

// ============================================
// PART 5: APP CONFIGURATION
// ============================================

import { ApplicationConfig } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([authInterceptor, loggingInterceptor]),
    ),
  ],
};

The example demonstrates the complete flow: interceptors for authentication and logging, a service that encapsulates API calls with error handling, and a component that manages loading and error state through signals. The catchError in the service transforms HttpErrorResponse into a user-facing message, and the component subscribes to the Observable and updates its signals accordingly.


Quick Reference

HTTP Methods

MethodPurposeBody
get<T>(url)Retrieve dataNo
post<T>(url, body)Create resourceYes
put<T>(url, body)Replace resourceYes
patch<T>(url, body)Partial updateYes
delete<T>(url)Remove resourceNo

Request Options

OptionTypePurpose
headersObject or HttpHeadersCustom headers
paramsObject or HttpParamsURL query parameters
responseType'json' | 'text' | 'blob' | 'arraybuffer'Expected response format
observe'body' | 'response' | 'events'What to return
withCredentialsbooleanSend cookies
reportProgressbooleanProgress events

Error Handling

StatusMeaning
0Network error, CORS, timeout
400Bad request
401Unauthorized
404Not found
500Server error

Interceptor Pattern

StepCode
Defineexport const auth: HttpInterceptorFn = (req, next) => next(req.clone({...}));
RegisterwithInterceptors([auth, logging])
OrderRuns in array order

Common Interceptor Uses

Use CasePurpose
AuthAdd Authorization header
LoggingLog requests and responses
LoadingShow spinner during requests
CachingReturn cached responses
RetryRetry failed requests
PrefixAdd base URL

Best Practices

โœ… Do This:

// Encapsulate API calls in a service
@Injectable({ providedIn: 'root' })
export class UserService {
  private readonly http = inject(HttpClient);
}                                                              // โœ…

// Subscribe to mutation requests
this.http.post('/api/users', user).subscribe();                // โœ…

// Use catchError for error handling
.pipe(catchError(this.handleError))                            // โœ…

// Clone requests in interceptors
const cloned = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }); // โœ…

// Use async pipe in templates
users$ = this.userService.getUsers();                          // โœ…

// Handle status 0 as network error
if (error.status === 0) { /* network issue */ }                // โœ…

โŒ Don’t Do This:

// Don't expect the generic type to validate at runtime
this.http.get<User>('/api/user') // type is assertion only   // โš ๏ธ

// Don't forget to subscribe to mutations
this.http.post('/api/users', user); // nothing happens        // โš ๏ธ

// Don't mutate requests in interceptors
req.headers.set('Authorization', token); // immutable          // โš ๏ธ

// Don't subscribe without cleanup in components
this.http.get('/api').subscribe(); // leaks                   // โš ๏ธ

// Don't use withXhr in SSR
provideHttpClient(withXhr()) // deprecated on server          // โš ๏ธ

// Don't swallow errors with of() in catchError
catchError(() => of(null)) // hides failure                   // โš ๏ธ

Common Pitfalls

PitfallProblemSolution
Mutation not subscribedRequest never fires.subscribe()
Immutable headers not clonedRequest unchangedreq.clone({ headers: ... })
catchError returns of(null)Error swallowedReturn throwError
Status 0 treated as server errorWrong diagnosisCheck for status 0
responseType not literalType inference brokenas const
Interceptor doesn’t call nextRequest chain brokenAlways return next(...)
Memory leak from manual subscribeSubscription not cleanedUse async pipe or takeUntil
Generic type not validatedRuntime surpriseValidate with unknown

Real-World Examples

1. Fetch list

this.http.get<User[]>('/api/users')

2. Create resource

this.http.post<User>('/api/users', newUser)

3. Update resource

this.http.put<User>(`/api/users/${id}`, updated)

4. Delete resource

this.http.delete<void>(`/api/users/${id}`)

5. Auth interceptor

export const auth: HttpInterceptorFn = (req, next) =>
  next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }))

6. Loading interceptor

export const loading: HttpInterceptorFn = (req, next) => {
  spinner.show();
  return next(req).pipe(finalize(() => spinner.hide()));
}

7. Retry interceptor

return next(req).pipe(retry(3))

8. Cache interceptor

if (req.method === 'GET' && cache.has(req.url)) return of(cache.get(req.url))

9. URL prefix interceptor

const apiReq = req.clone({ url: `https://api.example.com${req.url}` })

10. Full response with headers

this.http.get<User>('/api/user', { observe: 'response' })

Visual: Request Flow with Interceptors

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Component calls service.getUsers()                       โ”‚
โ”‚       โ”‚                                                   โ”‚
โ”‚       โ–ผ                                                   โ”‚
โ”‚  HttpClient.get('/api/users')                             โ”‚
โ”‚       โ”‚                                                   โ”‚
โ”‚       โ–ผ                                                   โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚  INTERCEPTOR CHAIN                                   โ”‚  โ”‚
โ”‚  โ”‚                                                     โ”‚  โ”‚
โ”‚  โ”‚  authInterceptor                                    โ”‚  โ”‚
โ”‚  โ”‚    โ””โ”€โ”€ clone req, add Authorization header          โ”‚  โ”‚
โ”‚  โ”‚         โ”‚                                           โ”‚  โ”‚
โ”‚  โ”‚         โ–ผ                                           โ”‚  โ”‚
โ”‚  โ”‚  loggingInterceptor                                 โ”‚  โ”‚
โ”‚  โ”‚    โ””โ”€โ”€ log start time                               โ”‚  โ”‚
โ”‚  โ”‚         โ”‚                                           โ”‚  โ”‚
โ”‚  โ”‚         โ–ผ                                           โ”‚  โ”‚
โ”‚  โ”‚  errorInterceptor                                   โ”‚  โ”‚
โ”‚  โ”‚    โ””โ”€โ”€ catchError, transform error                  โ”‚  โ”‚
โ”‚  โ”‚         โ”‚                                           โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚            โ”‚                                              โ”‚
โ”‚            โ–ผ                                              โ”‚
โ”‚  Backend /api/users                                       โ”‚
โ”‚            โ”‚                                              โ”‚
โ”‚            โ–ผ                                              โ”‚
โ”‚  Response flows back through interceptors in reverse      โ”‚
โ”‚            โ”‚                                              โ”‚
โ”‚            โ–ผ                                              โ”‚
โ”‚  Subscriber receives data or error                        โ”‚
โ”‚                                                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Cold Observable

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  const request = this.http.get('/api/users');             โ”‚
โ”‚       โ”‚                                                   โ”‚
โ”‚       โ”‚  No request sent.                                 โ”‚
โ”‚       โ”‚  Observable is a description.                     โ”‚
โ”‚       โ”‚                                                   โ”‚
โ”‚       โ–ผ                                                   โ”‚
โ”‚  request.subscribe({                                     โ”‚
โ”‚    next: (data) => console.log(data),                     โ”‚
โ”‚    error: (err) => console.error(err),                    โ”‚
โ”‚  });                                                      โ”‚
โ”‚       โ”‚                                                   โ”‚
โ”‚       โ”‚  NOW the request fires.                           โ”‚
โ”‚       โ”‚                                                   โ”‚
โ”‚       โ–ผ                                                   โ”‚
โ”‚  Backend receives GET /api/users                          โ”‚
โ”‚                                                          โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  MUTATIONS MUST BE SUBSCRIBED                             โ”‚
โ”‚                                                          โ”‚
โ”‚  this.http.post('/api/users', user); // โŒ no request     โ”‚
โ”‚  this.http.post('/api/users', user).subscribe(); // โœ…    โ”‚
โ”‚                                                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Error Handling Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Request fails                                            โ”‚
โ”‚       โ”‚                                                   โ”‚
โ”‚       โ–ผ                                                   โ”‚
โ”‚  HttpErrorResponse                                        โ”‚
โ”‚       โ”‚                                                   โ”‚
โ”‚       โ”œโ”€โ”€ status === 0  โ”€โ”€โ–บ Network/CORS error            โ”‚
โ”‚       โ”‚                                                   โ”‚
โ”‚       โ”œโ”€โ”€ status === 401 โ”€โ”€โ–บ Unauthorized                 โ”‚
โ”‚       โ”‚                                                   โ”‚
โ”‚       โ”œโ”€โ”€ status === 404 โ”€โ”€โ–บ Not found                    โ”‚
โ”‚       โ”‚                                                   โ”‚
โ”‚       โ””โ”€โ”€ status >= 500 โ”€โ”€โ–บ Server error                  โ”‚
โ”‚                                                          โ”‚
โ”‚       โ–ผ                                                   โ”‚
โ”‚  catchError((error: HttpErrorResponse) => {               โ”‚
โ”‚    // Log, transform, or re-throw                         โ”‚
โ”‚    return throwError(() => new Error('User message'));    โ”‚
โ”‚  })                                                       โ”‚
โ”‚                                                          โ”‚
โ”‚  Subscriber's error callback receives the error           โ”‚
โ”‚                                                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: When to Use HttpClient vs httpResource

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Do you need signal-based reactive state?                 โ”‚
โ”‚       โ”‚                                                   โ”‚
โ”‚       โ”œโ”€โ”€ Yes โ”€โ”€โ–บ httpResource                            โ”‚
โ”‚       โ”‚              - Automatic refetch on signal change โ”‚
โ”‚       โ”‚              - Loading/error state signals        โ”‚
โ”‚       โ”‚              - Cancellation on change             โ”‚
โ”‚       โ”‚                                                   โ”‚
โ”‚       โ””โ”€โ”€ No โ”€โ”€โ”€โ–บ HttpClient                              โ”‚
โ”‚                      - Full control over subscription    โ”‚
โ”‚                      - Manual state management           โ”‚
โ”‚                      - Transform with RxJS operators     โ”‚
โ”‚                                                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
ProviderprovideHttpClient()
Injectioninject(HttpClient)
Methodsget, post, put, patch, delete
Return typeCold Observable<T>
SubscriptionRequired for execution
Error typeHttpErrorResponse
Status 0Network/CORS failure
InterceptorHttpInterceptorFn
RegistrationwithInterceptors([...])
OrderingArray order
Immutabilityreq.clone() required

Key takeaways:

  • HttpClient is provided through provideHttpClient() and injected wherever needed โ€” no module import required in modern Angular
  • Requests are cold Observables โ€” no request fires until .subscribe() is called, and mutation requests must be subscribed to execute
  • The generic type parameter is a type assertion only โ€” it describes the expected response but does not validate it at runtime
  • HttpErrorResponse distinguishes failure modes through its status property โ€” status 0 is a network or CORS error, while 4xx and 5xx come from the server
  • Interceptors are functions that run for every request โ€” they can modify requests, transform responses, handle errors, and short-circuit the chain
  • Functional interceptors are preferred over class-based for their predictable ordering and injection context compatibility
  • Requests are immutable โ€” interceptors must clone them with req.clone() before modifying headers or other properties
  • The service pattern is recommended โ€” encapsulate API calls in injectable services rather than components
  • Always handle errors with catchError and return throwError to keep the error chain alive
  • Use the async pipe or explicit cleanup for component subscriptions to prevent memory leaks

Remember: HttpClient is the bridge between Angular applications and backend APIs. Requests are Observables, interceptors are middleware, and errors are structured. The cold Observable characteristic is the most common source of confusion โ€” nothing happens until you subscribe. Once that is internalized, the rest is configuration: headers, parameters, response types, and the interceptor chain that makes authentication, logging, and error handling reusable across every request.


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!