Skip to content
Angular ng http 4 min read

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.

CallReturnsBody 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 HttpClient directly from components.
  • Prefer the async pipe or toSignal() over manual subscribe() to avoid subscription leaks.
  • Use track in @for loops over fetched collections to keep DOM updates efficient.
  • Build URLs with HttpParams instead 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.
Last updated June 14, 2026
Was this helpful?