Angular's built-in HttpClient is the standard way to call a REST API — its Observable-based interface is the one detail that trips up developers coming from a Promise-based background, since nothing actually happens until something subscribes.
Setting up HttpClient
// app.config.ts (standalone) or app.module.ts (NgModule-based)
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [provideHttpClient()],
};
A basic GET request
@Injectable({ providedIn: 'root' })
export class ProductService {
constructor(private http: HttpClient) {}
getProducts(): Observable {
return this.http.get('https://api.example.com/products');
}
}
Subscribing to actually trigger the request
ngOnInit(): void {
this.productService.getProducts().subscribe(products => {
this.products = products;
});
}
This is the critical, genuinely common gotcha: HttpClient methods return a "cold" Observable that does nothing on its own — the actual HTTP request only fires once .subscribe() is called; calling getProducts() without subscribing to the result silently sends no request at all.
A POST request with a body
createProduct(product: Partial): Observable {
return this.http.post('https://api.example.com/products', product);
}
Adding query parameters
import { HttpParams } from '@angular/common/http';
searchProducts(query: string): Observable {
const params = new HttpParams().set('q', query).set('limit', 10);
return this.http.get('https://api.example.com/products', { params });
}
Adding custom headers
import { HttpHeaders } from '@angular/common/http';
getOrders(): Observable {
const headers = new HttpHeaders().set('Authorization', `Bearer ${this.token}`);
return this.http.get('https://api.example.com/orders', { headers });
}
Handling errors
this.productService.getProducts().subscribe({
next: products => this.products = products,
error: err => console.error('Failed to load products', err),
});
The object form of subscribe() (with next/error/complete keys), rather than passing a single callback function, is the modern recommended syntax — it makes handling an error case explicit and impossible to accidentally skip, unlike the older two-positional-argument form.
Transforming the response with RxJS operators
getActiveProducts(): Observable {
return this.http.get('https://api.example.com/products').pipe(
map(products => products.filter(p => p.isActive))
);
}
Piping the Observable through map() transforms the data before it ever reaches the subscriber — this keeps filtering/transformation logic centralized in the service rather than repeated in every component that calls it.
Unsubscribing to avoid a memory leak
private subscription = new Subscription();
ngOnInit(): void {
this.subscription.add(
this.productService.getProducts().subscribe(products => this.products = products)
);
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
An HTTP request Observable normally completes on its own after emitting once, but manually unsubscribing is still worth doing for a long-lived component where the request might still be in flight when the component is destroyed — the async pipe in a template handles this automatically and is generally the simpler choice when the data is only needed for display.