This post explains how to perform GET requests in Angular using HttpClient
. It includes clear headings and code examples only — no icons or extra visuals. The content covers basic usage, typing responses, query parameters, headers, error handling, RxJS operators, cancellation, caching, testing, performance considerations, security, and best practices.
Table of contents
- Introduction
- Setting up HttpClientModule
- Creating a typed service for GET requests
- Calling the service from a component
- Handling query parameters and headers
- Mapping and transforming responses with RxJS
- Error handling strategies
- Cancellation and aborting requests
- Caching GET responses
- Pagination and infinite scroll (GET patterns)
- Testing HTTP GET requests
- Performance tips and network considerations
- Security, CORS, and sensitive data
- Advanced patterns: interceptors and retry/backoff
- Server-side rendering considerations (Angular Universal)
- Real-world examples and patterns
- Summary and best practices
1. Introduction
GET requests are used to retrieve data from servers. In Angular, the HttpClient
service (from @angular/common/http
) provides a modern, typed, and RxJS-friendly API to make HTTP calls. This post focuses on practical patterns for GET requests: typed responses, streaming and mapping data, error handling, cancelation, caching, and testing.
2. Setting up HttpClientModule
Before using HttpClient
in your services or components, import HttpClientModule
in your root module (or the feature module where you plan to use HTTP).
// app.module.ts
import { NgModule } from ‘@angular/core’;
import { BrowserModule } from ‘@angular/platform-browser’;
import { HttpClientModule } from ‘@angular/common/http’;
import { AppComponent } from ‘./app.component’;
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, HttpClientModule],
bootstrap: [AppComponent]
})
export class AppModule {}
Once imported, you can inject HttpClient
into services or components.
3. Creating a typed service for GET requests
Always type your responses. Define interfaces that match your API responses so TypeScript can help you catch mistakes.
// models/user.ts
export interface Address {
street: string;
suite?: string;
city: string;
zipcode?: string;
}
export interface User {
id: number;
name: string;
username: string;
email: string;
address?: Address;
}
Create a service that performs GET requests. Use generics with HttpClient.get<T>()
to strongly type the response.
// services/user.service.ts
import { Injectable } from ‘@angular/core’;
import { HttpClient, HttpParams, HttpHeaders } from ‘@angular/common/http’;
import { Observable } from ‘rxjs’;
import { User } from ‘../models/user’;
@Injectable({ providedIn: ‘root’ })
export class UserService {
private readonly baseUrl = ‘https://jsonplaceholder.typicode.com’;
constructor(private http: HttpClient) {}
getUsers(): Observable<User[]> {
return this.http.get<User[]>(${this.baseUrl}/users
);
}
getUserById(id: number): Observable<User> {
return this.http.get<User>(${this.baseUrl}/users/${id}
);
}
// Example with query params
searchUsers(query: string): Observable<User[]> {
const params = new HttpParams().set(‘q’, query);
return this.http.get<User[]>(${this.baseUrl}/users
, { params });
}
}
4. Calling the service from a component
Inject the service into your component and subscribe to the returned observables. Prefer to use the async
pipe in templates when possible to avoid manual subscriptions and memory leaks.
// components/user-list.component.ts
import { Component, OnInit } from ‘@angular/core’;
import { Observable } from ‘rxjs’;
import { User } from ‘../models/user’;
import { UserService } from ‘../services/user.service’;
@Component({
selector: ‘app-user-list’,
template: `
<div *ngIf=”users$ | async as users; else loading”>
<pre>{{ users | json }}</pre>
</div>
<ng-template #loading>Loading…</ng-template>
`
})
export class UserListComponent implements OnInit {
users$!: Observable<User[]>;
constructor(private userService: UserService) {}
ngOnInit(): void {
this.users$ = this.userService.getUsers();
}
}
If you must subscribe manually (for side effects), remember to unsubscribe or use takeUntil
/ firstValueFrom
/ take(1)
.
// components/manual-subscribe.component.ts
import { Component, OnDestroy, OnInit } from ‘@angular/core’;
import { Subscription } from ‘rxjs’;
import { UserService } from ‘../services/user.service’;
@Component({ selector: ‘app-manual’, template: ” })
export class ManualSubscribeComponent implements OnInit, OnDestroy {
private sub: Subscription | undefined;
constructor(private userService: UserService) {}
ngOnInit() {
this.sub = this.userService.getUsers().subscribe({
next: users => console.log(‘Users’, users),
error: err => console.error(‘Error’, err)
});
}
ngOnDestroy() {
this.sub?.unsubscribe();
}
}
5. Handling query parameters and headers
HttpParams
is immutable; always assign the resulting instance. Use HttpHeaders
to add headers.
// services/post.service.ts
import { HttpClient, HttpHeaders, HttpParams } from ‘@angular/common/http’;
import { Injectable } from ‘@angular/core’;
import { Observable } from ‘rxjs’;
@Injectable({ providedIn: ‘root’ })
export class PostService {
constructor(private http: HttpClient) {}
getPosts(page = 1, limit = 10, search?: string): Observable<any> {
let params = new HttpParams().set(‘_page’, String(page)).set(‘_limit’, String(limit));
if (search) {
params = params.set(‘q’, search);
}
const headers = new HttpHeaders({ ‘X-App-Version’: ‘1.0.0’ });
return this.http.get(‘/api/posts’, { params, headers });
}
}
Use URLSearchParams for server-side or non-Angular use, but prefer HttpParams
inside Angular services.
6. Mapping and transforming responses with RxJS
Use RxJS operators to transform or enrich data before it reaches components.
// services/user.service.ts (extended)
import { map } from ‘rxjs/operators’;
getUserNames(): Observable<string[]> {
return this.http.get<User[]>(${this.baseUrl}/users
).pipe(
map(users => users.map(u => u.name))
);
}
Combine requests with forkJoin
, combineLatest
, zip
, or switchMap
as required.
// example combining two independent GETs
import { forkJoin } from ‘rxjs’;
const users$ = this.http.get<User[]>(${baseUrl}/users
);
const posts$ = this.http.get<any[]>(${baseUrl}/posts
);
forkJoin({ users: users$, posts: posts$ }).subscribe(result => {
console.log(result.users, result.posts);
});
When reacting to route parameter changes, use switchMap
to cancel previous requests.
// route-driven example
this.route.paramMap.pipe(
map(params => Number(params.get(‘id’))),
switchMap(id => this.userService.getUserById(id))
).subscribe(user => this.user = user);
7. Error handling strategies
Handle errors with catchError
and return a safe fallback or rethrow as needed. Log errors centrally with an interceptor for shared behavior.
import { catchError } from ‘rxjs/operators’;
import { throwError, of } from ‘rxjs’;
getUsersSafe(): Observable<User[]> {
return this.http.get<User[]>(${this.baseUrl}/users
).pipe(
catchError(error => {
// handle error, log it, and return a fallback value
console.error(‘GET users failed’, error);
return of([] as User[]); // fallback
})
);
}
If the UI needs to show error messages, pass the error to the component via the observable and let the UI decide how to display it.
this.userService.getUsers().subscribe({
next: data => (this.users = data),
error: err => (this.loadError = err)
});
8. Cancellation and aborting requests
You can cancel HTTP requests using the takeUntil
pattern or AbortController
(Angular 14+ supports passing an AbortSignal
). Use switchMap
to cancel prior requests when a new one arrives.
// takeUntil example
import { Subject } from ‘rxjs’;
import { takeUntil } from ‘rxjs/operators’;
private destroy$ = new Subject<void>();
ngOnInit() {
this.userService.getUsers()
.pipe(takeUntil(this.destroy$))
.subscribe(users => (this.users = users));
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
// AbortController example (Angular supports ‘signal’ option)
const controller = new AbortController();
this.http.get(‘/api/long’, { signal: controller.signal }).subscribe();
// later
controller.abort();
Using switchMap
when handling rapidly changing inputs (search boxes, route params) ensures previous HTTP calls are cancelled.
9. Caching GET responses
Implement caching where appropriate to reduce network calls. Simple in-memory cache:
// services/cache.service.ts
import { Injectable } from ‘@angular/core’;
import { Observable, of } from ‘rxjs’;
import { tap } from ‘rxjs/operators’;
@Injectable({ providedIn: ‘root’ })
export class CacheService {
private cache = new Map<string, any>();
get<T>(key: string): T | undefined {
return this.cache.get(key);
}
set<T>(key: string, value: T): void {
this.cache.set(key, value);
}
getOrFetch<T>(key: string, fetch$: Observable<T>): Observable<T> {
const cached = this.get<T>(key);
if (cached) {
return of(cached);
}
return fetch$.pipe(tap(value => this.set(key, value)));
}
}
Use it in your service:
// services/user.service.ts (with cache)
getUsersCached() {
const key = ‘users_all’;
return this.cacheService.getOrFetch(key, this.http.get<User[]>(${this.baseUrl}/users
));
}
For larger apps, use an interceptor that caches GET requests based on URL and parameters and respects cache headers.
10. Pagination and infinite scroll (GET patterns)
Server-side pagination is preferred. Use query parameters for page
, limit
, offset
, or cursor-based pagination. Example of cursor-based pattern:
getItems(cursor?: string, limit = 20) {
let params = new HttpParams().set(‘limit’, String(limit));
if (cursor) params = params.set(‘cursor’, cursor);
return this.http.get<{ items: any[]; nextCursor?: string }>(/api/items
, { params });
}
Implement infinite scroll by requesting the next page when the user scrolls near the bottom, appending items to the list.
Leave a Reply