Skip to content
Angular ng http 4 min read

HttpClient Overview

Most Angular applications talk to a backend at some point — fetching data, submitting forms, syncing state with an API. Angular ships a dedicated HttpClient service that wraps the browser’s fetch/XMLHttpRequest machinery with a strongly typed, RxJS-based API. It handles JSON parsing, error normalization, request cancellation, interceptors, and testability out of the box, so you rarely need to reach for fetch directly.

What HttpClient gives you

HttpClient is a tree-shakable service that returns Observable streams instead of promises. That design unlocks a lot: you can cancel an in-flight request by unsubscribing, retry with RxJS operators, debounce typeahead calls, and compose requests declaratively. It also parses JSON responses automatically and surfaces failures as HttpErrorResponse objects with consistent shapes.

FeatureDescription
Typed responsesGeneric methods like get<User>() return Observable<User>
Automatic JSONResponse bodies are parsed to objects by default
InterceptorsPlug in auth headers, logging, retries, and error handling globally
CancellationUnsubscribing aborts the underlying request
TestabilityprovideHttpClientTesting gives a mock backend with no real network

Configuring provideHttpClient

In a standalone application there are no NgModules, so you register HttpClient once in your application config using provideHttpClient(). This makes the service injectable everywhere via inject() or constructor injection.

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

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withFetch(),
      withInterceptors([authInterceptor]),
    ),
  ],
};
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig);

The provideHttpClient() function accepts optional features that customize behavior:

FeaturePurpose
withFetch()Use the modern fetch API instead of XMLHttpRequest
withInterceptors([...])Register functional interceptors
withInterceptorsFromDi()Bridge legacy class-based DI interceptors
withJsonpSupport()Enable JSONP requests for cross-origin reads
withXsrfConfiguration({...})Customize CSRF/XSRF token handling

Prefer withFetch() for new apps. It enables server-side rendering streaming and aligns Angular with the platform’s standard networking API.

Injecting and using HttpClient

Once provided, inject HttpClient into any component or service. Centralizing API calls in an injectable service keeps components lean and makes the data layer reusable.

// user.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

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

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

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

  getUser(id: number): Observable<User> {
    return this.http.get<User>(`${this.baseUrl}/${id}`);
  }
}

A component subscribes to the observable. The cleanest modern approach is the async pipe combined with signals, which avoids manual subscription management.

// users.component.ts
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { UserService } from './user.service';

@Component({
  selector: 'app-users',
  standalone: true,
  template: `
    @if (users(); as list) {
      <ul>
        @for (user of list; track user.id) {
          <li>{{ user.name }} — {{ user.email }}</li>
        }
      </ul>
    } @else {
      <p>Loading users…</p>
    }
  `,
})
export class UsersComponent {
  private userService = inject(UserService);
  users = toSignal(this.userService.getUsers());
}

Output:

- Ada Lovelace — [email protected]
- Alan Turing — [email protected]
- Grace Hopper — [email protected]

How responses are typed

The generic type argument tells TypeScript the shape of the parsed body — it does not validate at runtime, so the API still returns plain JSON. If you need the full response (status code, headers), pass { observe: 'response' } to get an HttpResponse<T> instead of the body alone.

this.http.get<User[]>(this.baseUrl, { observe: 'response' }).subscribe((res) => {
  console.log(res.status);          // 200
  console.log(res.headers.get('x-total-count'));
  console.log(res.body?.length);    // number of users
});

HttpClient observables are cold and single-shot: the request fires only on subscription, and each subscription triggers a new request. Use shareReplay if you need to multicast one response to several subscribers.

Best practices

  • Register HttpClient once with provideHttpClient(withFetch()) in app.config.ts rather than scattering providers.
  • Wrap API calls in @Injectable({ providedIn: 'root' }) services so components depend on a typed data layer, not raw URLs.
  • Always supply the generic type (get<T>()) for compile-time safety, but validate untrusted payloads at runtime when correctness matters.
  • Use the async pipe or toSignal() instead of manual subscribe() to avoid memory leaks and lifecycle bugs.
  • Centralize cross-cutting concerns — auth tokens, logging, retries — in functional interceptors rather than repeating them per call.
  • Unsubscribe (or rely on async/takeUntilDestroyed) so navigating away cancels in-flight requests.
Last updated June 14, 2026
Was this helpful?