Skip to content

httpResource (Angular 21)

httpResource is an Angular 21 primitive from @angular/common/http that provides declarative HTTP data loading. It returns a Resource signal that automatically handles loading states, errors, and provides reactive updates.

  • ✅ Declarative data loading - URL-based resource definition
  • ✅ Automatic state management - Loading, error, and value states
  • ✅ Signal-based - Returns Resource<T> signal
  • ✅ Manual reload - Trigger refetch with reload()
  • ✅ Type-safe - Full TypeScript support
  • ✅ Simpler than HttpClient - Less boilerplate
Feature HttpClient httpResource
API Observable-based Signal-based Resource
Loading state Manual Automatic
Error state Manual try/catch Automatic
Subscription Required Not required
Cleanup Manual unsubscribe Automatic
Result type Observable<T> Resource<T>
Reload Re-call method Call .reload()

import { Injectable, inject } from '@angular/core';
import { HttpClient, httpResource } from '@angular/common/http';
import { Album } from '../models/album.model';
@Injectable({ providedIn: 'root' })
export class AlbumService {
http = inject(HttpClient);
// Define the httpResource
readonly _albumsResource = httpResource<Album[]>(() => {
return 'http://localhost:3000/albums';
}, { defaultValue: [] });
// Expose as a getter
get albumsResource() {
return this._albumsResource;
}
// Traditional HttpClient method (for comparison)
findAll(): Observable<Album[]> {
return this.http.get<Album[]>('http://localhost:3000/albums');
}
}

Key points:

  • Returns a URL string from the loader function
  • Automatically creates a GET request
  • defaultValue provides initial value before data loads
  • Resource is read-only, exposed via getter

A common pattern is to reload the resource after creating, updating, or deleting items:

@Injectable({ providedIn: 'root' })
export class AlbumService {
http = inject(HttpClient);
// Resource for GET
readonly _albumsResource = httpResource<Album[]>(() => {
return 'http://localhost:3000/albums';
}, { defaultValue: [] });
get albumsResource() {
return this._albumsResource;
}
// Traditional methods for mutations
save(album: Album): Observable<Album> {
return this.http.post<Album>('http://localhost:3000/albums', album);
}
update(album: Album): Observable<Album> {
return this.http.put<Album>(`http://localhost:3000/albums/${album.id}`, album);
}
delete(id: number): Observable<void> {
return this.http.delete<void>(`http://localhost:3000/albums/${id}`);
}
}

Provide initial data while loading:

readonly albumsResource = httpResource<Album[]>(() => {
return 'http://localhost:3000/albums';
}, {
defaultValue: [] // Start with empty array, not undefined
});

Benefits:

  • Prevents undefined errors in templates
  • Can show placeholder data
  • Smoother UX during initial load

Use computed() to derive additional signals:

export class AlbumListComponent {
albumsResource = this.albumService.albumsResource;
// Extract value
albums = computed(() => this.albumsResource.value() ?? []);
// Derived computations
totalAlbums = computed(() => this.albums().length);
highlightedAlbums = computed(() =>
this.albums().filter(a => a.highlighted)
);
totalPrice = computed(() =>
this.albums().reduce((sum, a) => sum + a.price, 0)
);
averagePrice = computed(() => {
const albums = this.albums();
return albums.length > 0
? this.totalPrice() / albums.length
: 0;
});
}
template: `
@if (albumsResource.error(); as err) {
<div class="error-banner">
<h3>⚠️ Failed to load albums</h3>
<p>{{ err.message }}</p>
<button (click)="albumsResource.reload()">
🔄 Retry
</button>
</div>
}
`
template: `
@if (albumsResource.isLoading()) {
<div class="skeleton-grid">
@for (i of [1,2,3,4,5,6]; track i) {
<div class="skeleton-card">
<div class="skeleton-line"></div>
<div class="skeleton-line"></div>
</div>
}
</div>
}
@if (albums(); as albumList) {
<!-- Real content -->
}
`

Feature httpResource rxResource
Import from @angular/common/http @angular/core/rxjs-interop
Loader Returns URL string Returns Observable
HTTP Method GET only Any (via Observable)
Use case Simple GET requests Complex Observable chains
Example () => '/api/albums' () => http.get('/api/albums')

When to use httpResource:

  • ✅ Simple GET requests
  • ✅ URL-based loading
  • ✅ No complex RxJS operators needed

When to use rxResource:

  • ✅ POST, PUT, DELETE requests
  • ✅ Complex Observable pipelines
  • ✅ Need for RxJS operators (map, filter, etc.)
  • ✅ Multiple HTTP calls combined

// ❌ HttpClient approach - Manual state management
@Component({
selector: 'app-album-list',
template: `
@if (loading) {
<p>Loading...</p>
}
@if (error) {
<p class="error">{{ error }}</p>
}
@if (!loading && !error) {
@for (album of albums; track album.id) {
<div>{{ album.name }}</div>
}
}
`
})
export class AlbumListComponent implements OnInit, OnDestroy {
albums: Album[] = [];
loading = false;
error: string | null = null;
private destroy$ = new Subject<void>();
ngOnInit() {
this.loadAlbums();
}
loadAlbums() {
this.loading = true;
this.error = null;
this.albumService.findAll()
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (data) => {
this.albums = data;
this.loading = false;
},
error: (err) => {
this.error = err.message;
this.loading = false;
}
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}

Issues:

  • Manual loading/error state management
  • Need lifecycle hooks (OnInit, OnDestroy)
  • Manual subscription management
  • Manual cleanup with takeUntil
  • More boilerplate code

Complete example from student project (step-17):

album.service.ts
@Injectable({ providedIn: 'root' })
export class AlbumService {
http = inject(HttpClient);
readonly _albumsResource = httpResource<Album[]>(() => {
console.log('Loading albums...');
return 'http://localhost:3000/albums';
}, { defaultValue: [] });
get albumsResource() {
return this._albumsResource;
}
save(album: Album): Observable<Album> {
return this.http.post<Album>('http://localhost:3000/albums', album);
}
delete(id: number): Observable<void> {
return this.http.delete<void>(`http://localhost:3000/albums/${id}`);
}
}
album-list.component.ts
@Component({
selector: 'app-album-list',
standalone: true,
imports: [CommonModule, CurrencyPipe],
template: `
<div class="container">
<h1>Album Catalog</h1>
@if (albumsResource.isLoading()) {
<div class="loading">
<p>Loading albums...</p>
</div>
}
@if (albumsResource.error(); as err) {
<div class="error">
<p>Failed to load albums: {{ err.message }}</p>
<button (click)="albumsResource.reload()">Retry</button>
</div>
}
@if (albums(); as albumList) {
<div class="stats">
<p>Total Albums: {{ totalAlbums() }}</p>
<p>Average Price: {{ averagePrice() | currency }}</p>
</div>
<div class="album-grid">
@for (album of albumList; track album.id) {
<div class="album-card">
<h3>{{ album.name }}</h3>
<p class="artist">{{ album.artist }}</p>
<p class="price">{{ album.price | currency }}</p>
<div class="tags">
@for (tag of album.tags; track tag) {
<span class="tag">{{ tag }}</span>
}
</div>
<button (click)="deleteAlbum(album.id)">Delete</button>
</div>
}
</div>
}
</div>
`
})
export class AlbumListComponent {
private albumService = inject(AlbumService);
albumsResource = this.albumService.albumsResource;
albums = computed(() => this.albumsResource.value() ?? []);
totalAlbums = computed(() => this.albums().length);
averagePrice = computed(() => {
const albums = this.albums();
if (albums.length === 0) return 0;
return albums.reduce((sum, a) => sum + a.price, 0) / albums.length;
});
deleteAlbum(id: number) {
this.albumService.delete(id).subscribe({
next: () => this.albumsResource.reload(),
error: (err) => console.error('Delete failed:', err)
});
}
}

  • ✅ Use httpResource for simple GET requests
  • ✅ Provide defaultValue to avoid undefined errors
  • ✅ Keep resource private (_albumsResource), expose via getter
  • ✅ Use computed() to derive values from resource
  • ✅ Call reload() after mutations (POST, PUT, DELETE)
  • ✅ Handle errors gracefully in template with retry option
  • ✅ Show loading states for better UX
  • ❌ Don’t mutate resource data directly
  • ❌ Don’t use for POST/PUT/DELETE (use HttpClient instead)
  • ❌ Don’t forget to provide type parameter: httpResource<Album[]>()

albumsResource = this.albumService.albumsResource;
albums = computed(() => this.albumsResource.value() ?? []);
// Filtered views
rockAlbums = computed(() =>
this.albums().filter(a => a.tags.includes('Rock'))
);
highlightedAlbums = computed(() =>
this.albums().filter(a => a.highlighted)
);
refreshAlbums() {
this.albumsResource.reload();
}
createAlbum(album: Album) {
this.albumService.save(album).subscribe({
next: () => this.albumsResource.reload()
});
}
template: `
@if (albumsResource.error()) {
<div class="error-banner">
<p>Something went wrong!</p>
<button (click)="albumsResource.reload()">Try Again</button>
</div>
}
`


See this pattern in action:

  • Service: src/app/services/album.service.ts
  • Component: src/app/components/albums/album-list/album-list.component.ts
  • Learning Path: Day 3, Module 3.6 - Resource API

Last Updated: December 2024 Angular Version: 21+ Status: Current best practice for declarative HTTP loading