Site icon Ampersand Tutorials

Angular HTTP Client and REST APIs (2026)

Quick answer: Enable the Angular HTTP client by adding provideHttpClient() to app.config.ts, then inject HttpClient in a service and call get, post, put or delete. Every method returns an Observable, so subscribe or use firstValueFrom / resource to get the value. Add functional interceptors to attach tokens and handle errors in one place.The sections below follow the Angular HTTP client in the order you wire it: enabling it, wrapping calls in a service, interceptors, then retries and error handling, which is where most Angular HTTP client bugs hide.

Enable the Angular HTTP client

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './auth.interceptor';

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

The old HttpClientModule is deprecated. In current Angular, HttpClient is available by default once the app is created, but you still call provideHttpClient() when you want to register interceptors or features such as withFetch().

Wrap Angular HTTP client calls in a service

Never call HttpClient directly from a component. Put the calls in a service, keep the endpoints in one place, and let components ask for data.

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

export interface Product { id: number; name: string; price: number; }

@Injectable({ providedIn: 'root' })
export class ProductService {
  private http = inject(HttpClient);
  private base = '/api/products';

  products = signal<Product[]>([]);
  loading = signal(false);
  error = signal<string | null>(null);

  async load() {
    this.loading.set(true);
    this.error.set(null);
    try {
      this.products.set(await firstValueFrom(this.http.get<Product[]>(this.base)));
    } catch {
      this.error.set('Could not load products.');
    } finally {
      this.loading.set(false);
    }
  }

  get(id: number) {
    return this.http.get<Product>(`${this.base}/${id}`);
  }

  create(product: Omit<Product, 'id'>) {
    return this.http.post<Product>(this.base, product);
  }

  update(product: Product) {
    return this.http.put<Product>(`${this.base}/${product.id}`, product);
  }

  remove(id: number) {
    return this.http.delete<void>(`${this.base}/${id}`);
  }
}

Two habits make this robust.

First, type the response with a generic — http.get<Product[]> — so the rest of the app knows the shape. Second, hold the result in a signal and expose loading and error signals next to it, so the template can show a spinner or a message without extra flags.

Interceptors: cross-cutting concerns in one place

An interceptor sees every request and response. This is where you attach an auth token and centralise error handling, instead of repeating it in every service. Modern Angular uses functional interceptors.

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

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthService).token();
  const authReq = token
    ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
    : req;

  return next(authReq).pipe(
    catchError(err => {
      if (err.status === 401) inject(AuthService).logout();
      return throwError(() => err);
    }),
  );
};

Requests are immutable, so you clone the request to add a header rather than mutating it. One interceptor now covers authentication and 401 handling for the whole application.

Retry and error handling

Transient network failures are common, so retry a few times with a delay before giving up. RxJS operators do this declaratively.

import { retry, timer } from 'rxjs';

this.http.get<Product[]>(this.base).pipe(
  retry({ count: 3, delay: (err, n) => timer(n * 1000) }),
);

That retries three times with a growing delay of one, two and three seconds, then surfaces the error. Pair it with the interceptor’s catchError to keep failure handling consistent.

Continue with signals and state management to decide where fetched data should live. To build an app against a real API with guidance, Ampersand Academy teaches Angular one-to-one.

Frequently asked questions

Do I still need HttpClientModule in Angular?

No. HttpClientModule is deprecated. Use provideHttpClient() in app.config.ts, optionally with feature functions such as withInterceptors.

Why does HttpClient return an Observable?

Observables are lazy and cancellable, and they compose with RxJS operators such as retry and catchError. Convert to a Promise with firstValueFrom when async and await suits you better.

What is an HTTP interceptor in Angular?

A function that runs on every request and response, used to add headers such as an auth token or to handle errors centrally. Register it with provideHttpClient(withInterceptors([…])).

How do I retry a failed HTTP request in Angular?

Pipe the request through the retry operator with a count and delay, for example retry with count 3 and a delay of a few seconds, then handle the final error in the interceptor.

Where should HTTP calls live in Angular?

In a service, never directly in a component. The service keeps endpoints in one place, types the responses and exposes loading and error state to the components that inject it.

Exit mobile version