Reader Stacks

Calling an API in Angular With HttpClient

HttpClient returns a cold Observable — nothing is actually sent over the network until something subscribes to it, a genuinely common source of confusion for anyone expecting a call to fire the moment it's written.

Calling an API in Angular With HttpClient

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.