HttpClient
HttpClient is Angular’s built-in way to call APIs. It returns typed Observables, plugs into interceptors, and works nicely with signals.
On this page · 6 min · Intermediate
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Enable HttpClient with provideHttpClient
- Send typed GET, POST, PUT and DELETE requests
- Pass query parameters and headers
- Expose results as signals and handle errors
Explanation#
Add provideHttpClient() (optionally withFetch() and withInterceptors([...])) to app.config.ts, then inject(HttpClient) in a service.
Requests are typed: http.get<Product[]>("/api/products"). Nothing is sent until something subscribes — the async pipe, toSignal, or .subscribe().
Options: { params: { page: 2, q: "pen" } } builds the query string; { headers: {...} } adds headers. post(url, body) sends JSON automatically.
toSignal(observable, { initialValue: [] }) turns a request into a signal you can read in templates without manual subscriptions.
Failed requests (network errors, 4xx/5xx) error the Observable with an HttpErrorResponse (status, error body). Handle them with catchError or in an interceptor.
Keep HTTP calls in services so components only call productApi.list().
Syntax#
// app.config.ts
providers: [provideHttpClient(withFetch())]
// product-api.ts
private http = inject(HttpClient);
list(q = '') { return this.http.get<Product[]>('/api/products', { params: { q } }); }
create(p: NewProduct) { return this.http.post<Product>('/api/products', p); }
Code example: A typed API service and a component using toSignal#
import { Component, Injectable, inject, signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { catchError, of } from 'rxjs';
export type Todo = { id: number; title: string; completed: boolean };
@Injectable({ providedIn: 'root' })
export class TodoApi {
private http = inject(HttpClient);
private base = 'https://jsonplaceholder.typicode.com/todos';
list(limit = 5) {
return this.http.get<Todo[]>(this.base, { params: { _limit: limit } });
}
create(title: string) {
return this.http.post<Todo>(this.base, { title, completed: false });
}
remove(id: number) {
return this.http.delete<void>(`${this.base}/${id}`);
}
}
@Component({
selector: 'app-todos',
template: `
@if (error()) { <p role="alert">{{ error() }}</p> }
<ul>
@for (t of todos(); track t.id) { <li>{{ t.completed ? '✅' : '⬜' }} {{ t.title }}</li> }
@empty { <li>Loading…</li> }
</ul>
<button (click)="add()">Add “Learn HttpClient”</button>
@if (created()) { <p>Created #{{ created()!.id }}: {{ created()!.title }}</p> }
`,
})
export class Todos {
private api = inject(TodoApi);
error = signal('');
created = signal<Todo | null>(null);
todos = toSignal(
this.api.list().pipe(
catchError((err: HttpErrorResponse) => {
this.error.set(`Could not load todos (${err.status || 'network error'}).`);
return of([] as Todo[]);
}),
),
{ initialValue: [] as Todo[] },
);
add() {
this.api.create('Learn HttpClient').subscribe({
next: todo => this.created.set(todo),
error: () => this.error.set('Could not create the todo.'),
});
}
}
// app.config.ts: providers: [provideHttpClient(withFetch())]
The list shows “Loading…”, then five todos with ✅/⬜ marks. Clicking the button POSTs JSON and shows “Created #201: Learn HttpClient” (the test API echoes it back). If the network is down, “Could not load todos (network error).” appears instead of crashing.
This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- TodoApi wraps every endpoint; get<Todo[]> types the response.
- params: { _limit: 5 } becomes ?_limit=5.
- toSignal subscribes to the request and exposes the result as a signal with an initial empty array.
- catchError turns an HTTP error into a friendly message and an empty list.
- post sends the object as JSON; subscribe triggers the request.
Real-world use case#
An Angular admin talks to an ASP.NET Core API through one service per resource (ProductApi, OrderApi). Components never build URLs, and an interceptor adds the auth token to every request.
Common mistakes#
provideHttpClient() is missing from app.config.ts.
HTTP Observables are cold: without subscribe, async or toSignal, no request is sent.
Calling api.list() in a template expression creates a new request on every change detection. Assign it once to a field or signal.
Best practices#
- provideHttpClient(withFetch()) in app.config
- Typed requests in API services
- toSignal or async pipe instead of manual subscriptions
- Handle errors where you can show a message
Remember this#
provideHttpClient() once; inject(HttpClient) in services.
No subscriber → no request.
toSignal(http.get(...), { initialValue }) for templates.
Practice exercise#
Your task: Add a search(q: string) method to a ProductApi that calls GET /api/products?q=… and returns Observable<Product[]>.
Need a hint?
Show solution
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
export type Product = { id: number; name: string; price: number };
@Injectable({ providedIn: 'root' })
export class ProductApi {
private http = inject(HttpClient);
search(q: string) {
return this.http.get<Product[]>('/api/products', { params: { q } });
}
}
Mini quiz#
When does http.get(...) send the request?
Want more? Practise all Angular MCQs · Angular interview questions
My notes#
Saved in this browser.