GET Requests
Reading data from a backend is the most common job in a frontend application, and Angular’s HttpClient.get() is the tool for it. A GET request asks the server for a resource and returns an RxJS Observable that emits the parsed response. Because GET is read-only, it never carries a request body — everything it needs travels in the URL and headers. This page covers issuing GET requests, typing the response with generics, and consuming the resulting observable in a modern standalone component.
Issuing a basic GET request
HttpClient is provided application-wide and pulled into your service or component with inject(). Calling get() returns a cold observable: nothing is sent over the network until something subscribes. By default Angular parses the response body as JSON, so for a JSON API you receive a ready-to-use object.
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
private base = 'https://api.example.com';
getUsers(): Observable<User[]> {
return this.http.get<User[]>(`${this.base}/users`);
}
}
Here User[] is the generic type argument. It does not validate the response at runtime — it is a compile-time contract that tells TypeScript what shape to expect, giving you autocompletion and type-checking downstream.
Typing the response with generics
The generic parameter flows through the whole pipeline. get<T>() produces an Observable<T>, so every operator and subscriber sees the correct type. Define an interface for the resource and pass it in.
export interface User {
id: number;
name: string;
email: string;
}
getUser(id: number): Observable<User> {
return this.http.get<User>(`${this.base}/users/${id}`);
}
Omitting the generic is legal but leaves you with Observable<Object>, which forces casts and loses safety. Always supply the type.
| Call | Returns | Body type seen in code |
|---|---|---|
get(url) | Observable<Object> | Object (untyped) |
get<User>(url) | Observable<User> | User |
get<User[]>(url) | Observable<User[]> | User[] |
get<Blob>(url, { responseType: 'blob' }) | Observable<Blob> | Blob |
The generic describes the parsed body only. It cannot guarantee the server actually sent that shape — for untrusted APIs, validate with a runtime schema library such as Zod before trusting the data.
Consuming the observable
You can subscribe manually, but in templates the async pipe is cleaner: it subscribes, renders emissions, and unsubscribes automatically when the component is destroyed. Combined with signals via toSignal(), it fits naturally into modern Angular.
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { UserService } from './user.service';
@Component({
selector: 'app-user-list',
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 UserListComponent {
private userService = inject(UserService);
users = toSignal(this.userService.getUsers());
}
toSignal() converts the observable to a signal that starts as undefined, then updates when the response arrives, driving the @if/@for control flow. No manual subscription, no cleanup code.
When you need the result imperatively — for example in a button handler — subscribe directly:
loadUser(id: number): void {
this.userService.getUser(id).subscribe((user) => {
console.log('Loaded:', user.name);
});
}
Output:
Loaded: Ada Lovelace
Requests fire once and complete
A key gotcha: HttpClient observables emit exactly one value and then complete. They do not poll or stay open. If you need fresh data, you must call the method again — typically by re-subscribing or by composing with an RxJS source like switchMap.
import { Subject, switchMap } from 'rxjs';
private refresh$ = new Subject<void>();
users = toSignal(
this.refresh$.pipe(switchMap(() => this.userService.getUsers())),
{ initialValue: [] as User[] }
);
reload(): void {
this.refresh$.next();
}
Each reload() pushes a new value through refresh$, and switchMap cancels any in-flight request before starting the next one — ideal for search-as-you-type or pull-to-refresh.
Best Practices
- Always pass a generic type argument to
get<T>()so the response is typed end to end. - Wrap HTTP calls in injectable services rather than calling
HttpClientdirectly from components. - Prefer the
asyncpipe ortoSignal()over manualsubscribe()to avoid subscription leaks. - Use
trackin@forloops over fetched collections to keep DOM updates efficient. - Build URLs with
HttpParamsinstead of manual string concatenation when adding query parameters. - Remember GET responses emit once and complete — re-trigger the request to refresh data.
- Validate responses from untrusted sources at runtime; generics are compile-time only.