Angular's HttpClient is the standard way to call a REST API — every method returns an RxJS Observable, not a Promise or a direct value, which is the single most important thing to understand before writing your first API call.
1. Provide HttpClient
In a standalone-component Angular app (17+), add it in app.config.ts:
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [provideHttpClient()],
};
Older, module-based apps import HttpClientModule into the relevant NgModule instead — same effect, different registration mechanism.
2. Making a request
import { HttpClient } from '@angular/common/http';
import { inject } from '@angular/core';
export class ProductService {
private http = inject(HttpClient);
getProducts() {
return this.http.get<Product[]>('/api/products');
}
}
3. Subscribing to get the actual data
this.productService.getProducts().subscribe({
next: (products) => this.products = products,
error: (err) => console.error('Failed to load products', err),
});
Nothing happens until .subscribe() is called — an Observable from HttpClient is "cold," meaning the actual HTTP request doesn't fire until something subscribes to it. Calling getProducts() alone, without subscribing, never sends the request at all.
Mistake 1: treating it like a synchronous value
// Wrong — products is still undefined here, the request hasn't resolved yet
const products = this.productService.getProducts();
console.log(products.length); // error or wrong result
The response only exists inside the subscribe() callback (or via the async pipe in a template) — code written as if getProducts() returns the array directly will run before the network request has actually completed.
Mistake 2: not unsubscribing where it matters
For a single one-off HTTP call, HttpClient's Observable completes automatically after emitting once, so manual unsubscription usually isn't necessary. It matters more for long-lived subscriptions (a WebSocket stream, a repeating timer) — those genuinely need to be unsubscribed in ngOnDestroy to avoid a memory leak, which is a different situation from a simple one-time API call.
Using the async pipe instead of manual subscription
products$ = this.productService.getProducts();
<li *ngFor="let product of products$ | async">{{ product.name }}</li>
The async pipe subscribes automatically when the template renders and unsubscribes automatically when the component is destroyed — often the cleaner choice over manual subscribe() calls specifically because it removes the cleanup concern entirely.