EthicsComputerCourses
Join training +91 90532 10052

HttpClient

Angular · Lesson 20 of 41 Intermediate 6 min read Updated

Most apps load and save data through a server API (application programming interface): a set of URLs that return JSON. Angular's HttpClient service sends these requests, turns JSON into typed objects and lets you plug in shared logic, such as adding a login token to every request.

In this lesson you will set up HttpClient, wrap an API in a service, handle errors and write interceptors. The next lesson shows how to load data into signals with resource and httpResource.

Set up HttpClient

Add provideHttpClient() to the providers in app.config.ts:

TypeScript
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

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

In Angular 22, HttpClient uses the browser's modern fetch() API by default. If you need upload progress events, which fetch cannot report, use the older XMLHttpRequest backend with provideHttpClient(withXhr()).

Wrap the API in a service

Keep all URLs and request code in a service, so components only call simple methods. Imagine a canteen backend (for example a PHP or Node.js app) that serves the menu at /api/menu:

TypeScript
import { HttpClient } from '@angular/common/http';
import { Service, inject } from '@angular/core';
import { Observable } from 'rxjs';

export interface MenuItem {
  id: number;
  name: string;
  price: number;
  veg: boolean;
}

@Service()
export class MenuApi {
  private http = inject(HttpClient);
  private baseUrl = '/api/menu';

  getAll(): Observable<MenuItem[]> {
    return this.http.get<MenuItem[]>(this.baseUrl);
  }

  search(term: string, vegOnly: boolean): Observable<MenuItem[]> {
    return this.http.get<MenuItem[]>(this.baseUrl, { params: { q: term, veg: vegOnly } });
  }

  add(item: Omit<MenuItem, 'id'>): Observable<MenuItem> {
    return this.http.post<MenuItem>(this.baseUrl, item);
  }

  updatePrice(id: number, price: number): Observable<MenuItem> {
    return this.http.patch<MenuItem>(`${this.baseUrl}/${id}`, { price });
  }

  remove(id: number): Observable<void> {
    return this.http.delete<void>(`${this.baseUrl}/${id}`);
  }
}
  • get<MenuItem[]>(url) says what type you expect the JSON to be. TypeScript trusts you here; it does not check the server's real response, so keep your interfaces in line with the API.
  • params builds the query string: search('dosa', true) requests /api/menu?q=dosa&veg=true.
  • post, put and patch take a body as the second argument and send it as JSON.

Observables: nothing happens until you subscribe

HttpClient methods return an RxJS Observable, a description of work that produces values over time. An observable from HttpClient is cold: no request is sent until something subscribes. Each subscription sends a new request.

TypeScript
import { HttpErrorResponse } from '@angular/common/http';
import { Component, inject, signal } from '@angular/core';
import { MenuApi, MenuItem } from './menu-api';

@Component({
  selector: 'app-menu-list',
  template: `
    @if (error()) {
      <p class="error">{{ error() }}</p>
    }
    <ul>
      @for (item of items(); track item.id) {
        <li>{{ item.name }} - ₹{{ item.price }}</li>
      }
    </ul>
    <button (click)="addChai()">Add Masala Chai</button>
  `,
})
export class MenuList {
  private api = inject(MenuApi);
  protected items = signal<MenuItem[]>([]);
  protected error = signal('');

  constructor() {
    this.api.getAll().subscribe({
      next: (data) => this.items.set(data),
      error: (err: HttpErrorResponse) => this.error.set(`Could not load the menu (status ${err.status}).`),
    });
  }

  protected addChai() {
    this.api.add({ name: 'Masala Chai', price: 15, veg: true }).subscribe((created) => {
      this.items.update((list) => [...list, created]);
    });
  }
}

If the server returns two items and the user then clicks the button, the list shows:

Output
Poha - ₹30
Upma - ₹35
Masala Chai - ₹15

If the server fails with status 500, the page shows "Could not load the menu (status 500)." An HttpErrorResponse has a status (0 means the network failed or the request was blocked) and an error body from the server.

HttpClient requests complete after one response, so you do not need to unsubscribe from them in most cases. Putting the result into a signal, as here, connects the observable world to the signal world. You will see shorter ways in Async Data with resource and RxJS & Signals Interop.

Interceptors

An interceptor is a function that sees every request before it is sent and every response after it arrives. It is the right place for logic that applies to all requests: adding an Authorization header, logging, showing a global loading bar or retrying. Interceptors are plain functions:

TypeScript
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { finalize } from 'rxjs';
import { AuthToken } from './auth-token';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthToken).value();
  if (!token || !req.url.startsWith('/api')) {
    return next(req);
  }
  return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));
};

export const timingInterceptor: HttpInterceptorFn = (req, next) => {
  const started = performance.now();
  return next(req).pipe(
    finalize(() => console.log(`${req.method} ${req.urlWithParams} took ${Math.round(performance.now() - started)} ms`)),
  );
};

Register them, in order, with withInterceptors:

TypeScript
provideHttpClient(withInterceptors([authInterceptor, timingInterceptor])),

Requests are immutable (cannot be changed), so an interceptor creates a modified copy with req.clone(...). The auth interceptor only adds the token for your own /api URLs, so it never leaks the token to other websites.

CORS and the development proxy

During development, your Angular app runs on localhost:4200 and your API may run on another port, such as localhost:8080. The browser treats these as different origins and blocks the response unless the server allows it, which is called CORS (cross-origin resource sharing). Either configure CORS on the server, or let the Angular dev server forward API calls with a proxy file proxy.conf.json:

JSON
{
  "/api": {
    "target": "http://localhost:8080",
    "secure": false
  }
}

Start it with ng serve --proxy-config proxy.conf.json. Your code keeps using /api/menu, and in production the API is usually served from the same domain.

Common mistakes

  • Forgetting provideHttpClient(). Injecting HttpClient then fails with a "No provider found" error (NG0201).
  • Calling a method without subscribing. this.api.add(item); alone sends nothing. Subscribe, or use resource / httpResource.
  • Subscribing twice by accident. Each subscription is a separate request, which can create duplicate orders.
  • Trusting the generic type. get<User[]> does not validate the JSON. Validate important data or handle unexpected shapes.

Key points

  • Add provideHttpClient() in app.config.ts; Angular 22 uses fetch by default (withXhr() for upload progress).
  • Put API calls in a service; get, post, put, patch and delete return typed observables.
  • Requests are sent only when you subscribe; handle failures with an error callback and HttpErrorResponse.
  • Functional interceptors (HttpInterceptorFn) registered with withInterceptors add shared behaviour such as auth headers.
  • Use a dev-server proxy or server-side CORS settings when the API runs on another origin.

Exercise

Write a StudentApi service for a school backend at /api/students with methods list(className: string) (GET with a class query parameter), create(student) (POST) and remove(id) (DELETE). Then write a functional interceptor that adds the header X-School-Code: RTK-042 to every request whose URL starts with /api.

Show solution
TypeScript
export interface Student {
  id: number;
  name: string;
  className: string;
}

@Service()
export class StudentApi {
  private http = inject(HttpClient);

  list(className: string) {
    return this.http.get<Student[]>('/api/students', { params: { class: className } });
  }

  create(student: Omit<Student, 'id'>) {
    return this.http.post<Student>('/api/students', student);
  }

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

export const schoolCodeInterceptor: HttpInterceptorFn = (req, next) =>
  req.url.startsWith('/api')
    ? next(req.clone({ setHeaders: { 'X-School-Code': 'RTK-042' } }))
    : next(req);

Register the interceptor with provideHttpClient(withInterceptors([schoolCodeInterceptor])). list('9B') requests /api/students?class=9B, and every one of these requests carries the school code header.

Quick Quiz

1. What must you add to app.config.ts before injecting HttpClient?

2. When does this.http.get('/api/menu') actually send the request?

3. Why does an interceptor call req.clone(...) to add a header?

4. What does an HttpErrorResponse with status 0 usually mean?

Learn Angular with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call