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.
| Feature | Description |
|---|---|
| Typed responses | Generic methods like get<User>() return Observable<User> |
| Automatic JSON | Response bodies are parsed to objects by default |
| Interceptors | Plug in auth headers, logging, retries, and error handling globally |
| Cancellation | Unsubscribing aborts the underlying request |
| Testability | provideHttpClientTesting 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:
| Feature | Purpose |
|---|---|
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
});
HttpClientobservables are cold and single-shot: the request fires only on subscription, and each subscription triggers a new request. UseshareReplayif you need to multicast one response to several subscribers.
Best practices
- Register
HttpClientonce withprovideHttpClient(withFetch())inapp.config.tsrather 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
asyncpipe ortoSignal()instead of manualsubscribe()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.