Angular Resource API: Master resource(), rxResource() & httpResource() from Scratch
Learn Angular's new reactive data-fetching APIs: resource(), rxResource(), and httpResource(). Discover how they replace traditional HTTP/Signal fetching, handle reloading and race conditions, and see real-world scenarios.
- 1. Angular Signals: The Complete Guide from Beginner to Advanced
- 2. Angular Without Zone.js: Signals, New Control Flow & Zoneless Change Detection
- 3. Angular Signal Forms: The In-Depth 'Zero to Hero' Guide
- 4. Angular Resource API: Master resource(), rxResource() & httpResource() from Scratch
Introduction
Remember building custom async wrappers on top of signals just to fetch data? If you followed our Angular Signals Guide — particularly the Custom Signal Utilities section — you know how much glue code that requires.
Good news: you can delete all of that now. With Angular 19+, Angular introduces the Resource API — three purpose-built primitives (resource(), rxResource(), and httpResource()) that handle async data fetching, loading states, errors, cancellation, and re-evaluation out of the box.
In this guide, we'll dissect each variant, explore how they manage requests under the hood, and build three production-ready, real-world examples you can drop into your apps today.
Why the Resource API? 💡
Before the Resource API, fetching data in Angular required writing boilerplate code. Whether using RxJS HttpClient or Promises, you had to manually track states:
// ❌ The old, manual way
@Component({...})
export class UserProfile {
userId = signal(1);
user = signal<User | null>(null);
isLoading = signal(false);
error = signal<string | null>(null);
constructor(private http: HttpClient) {
effect(() => {
this.isLoading.set(true);
this.http.get<User>(`/api/users/${this.userId()}`).subscribe({
next: (u) => {
this.user.set(u);
this.isLoading.set(false);
},
error: (err) => {
this.error.set(err.message);
this.isLoading.set(false);
}
});
});
}
}
This approach suffers from race conditions (if userId changes rapidly, multiple subscriptions run concurrently and can resolve out of order) and requires verbose state management.
The Resource API solves this declaratively:
// ✅ The modern, declarative Resource way
@Component({...})
export class UserProfile {
userId = signal(1);
userResource = httpResource(() => `/api/users/${this.userId()}`);
// Exposes read-only signals:
// userResource.value() -> User | undefined
// userResource.isLoading() -> boolean
// userResource.error() -> unknown
}
Anatomy of a Resource 🧬
A resource wraps an asynchronous data source and exposes it as a collection of read-only signals.
The State Signals
When you define a resource, you get an object containing the following signals:
value(): A read-only signal containing the latest resolved value, orundefinedif the resource has not yet resolved (or has errored).isLoading(): A read-only boolean signal indicating if a request is currently in progress.error(): A read-only signal containing the error thrown during the last fetch, orundefinedif no error occurred.status(): A read-only signal indicating the detailed state of the resource.
The Status Enum
The status() signal returns a value from the ResourceStatus enum:
| Status Value | Meaning |
|---|---|
ResourceStatus.Idle | The resource has been created but loader hasn't started (e.g., if parameter signal is undefined or null). |
ResourceStatus.Loading | The resource is loading its initial value. |
ResourceStatus.Resolved | The resource loader resolved successfully and value() contains the data. |
ResourceStatus.Error | The resource loader threw an error. |
ResourceStatus.Reloading | The resource already has a resolved value, but a parameter change or manual reload has triggered a new request. |
Deep Dive: The Three Resource APIs 🔍
Angular provides three helper functions to create resources, depending on the underlying async mechanism.
1. resource() - Promise-Based Async Tasks
The core resource() function is ideal for general-purpose asynchronous operations, such as interacting with browser Web APIs (e.g., Geolocation, Clipboard), using native fetch, or wrapping third-party Promise-based libraries.
import { resource } from '@angular/core';
const userResource = resource({
// The request parameter function. Any signals read here will trigger a re-fetch.
request: () => ({ id: this.userId() }),
// The loader function. It receives the request and returns a Promise.
loader: async ({ request, abortSignal }) => {
const response = await fetch(`/api/users/${request.id}`, { signal: abortSignal });
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
}
});
Note: Angular automatically supplies an abortSignal inside the loader context. If a new request is triggered before the current one finishes, the previous request is aborted automatically.
2. rxResource() - The RxJS Observable Bridge
If you are working with an existing service layer built on RxJS Observables, or if you need to use RxJS operators (like debounceTime, distinctUntilChanged, or custom retry logic), rxResource is your best friend. It bridges the gap between Observables and Signals.
rxResource lives in @angular/core/rxjs-interop:
import { rxResource } from '@angular/core/rxjs-interop';
const searchResults = rxResource({
request: () => ({ query: this.searchQuery() }),
loader: ({ request }) => {
// Return an Observable. rxResource automatically subscribes/unsubscribes.
return this.searchService.getResults(request.query);
}
});
3. httpResource() - Declarative HttpClient Wrapper
Introduced to work seamlessly with Angular's HttpClient, httpResource is the cleanest way to make HTTP GET requests. It automatically uses the configured HttpClient (including interceptors) and supports parameterization out of the box.
import { httpResource } from '@angular/common/http';
// Automatically maps to a GET request and resolves the response body
const weatherResource = httpResource(() => `/api/weather?city=${this.city()}`);
You can also pass options for advanced request customization:
const detailedUser = httpResource({
request: () => ({ id: this.userId() }),
loader: ({ request }) => ({
url: `/api/users/${request.id}`,
options: {
headers: { 'Authorization': `Bearer ${this.token()}` }
}
})
});
Triggering Re-evaluation & Parameterization ⚙️
Resources are automatically reactive. This reactivity is governed by the request function.
Reacting to Parameter Signals
If the request function reads any signals, the resource will set its dependencies. When those signals change, the resource automatically schedules a re-fetch.
// 1. Define a dependency signal
page = signal(1);
pageSize = signal(10);
// 2. Resource automatically tracks page() and pageSize()
products = httpResource(() => `/api/products?page=${this.page()}&size=${this.pageSize()}`);
If the request returns undefined (or a falsy value depending on your checks), the resource loader will not execute, putting the resource into an Idle state. This is highly useful for conditional fetching:
userId = signal<number | null>(null);
userProfile = httpResource(() => {
const id = this.userId();
// If no ID is available, return undefined to skip fetching
if (!id) return undefined;
return `/api/users/${id}`;
});
Manual Reloading via reload()
Sometimes you need to refresh data without changing the request parameters (e.g., a "Pull to Refresh" or after mutating data on the server). Every resource exposes a .reload() method:
// Trigger a manual reload
refreshData() {
this.products.reload();
}
During a reload, isLoading() is set to true, status() shifts to ResourceStatus.Reloading, but value() retains its previous value. This prevents layout thrashing by keeping old data visible until the new data arrives.
Real-Time Examples & Code Walkthroughs 🛠️
Let's look at three real-world, code-dense scenarios implemented with the Resource API.
Scenario 1: Auto-paginating Search Grid (httpResource)
Here we build a paginated table component. It automatically fetches new data when pagination changes or the user searches.
import { Component, signal, computed } from '@angular/core';
import { httpResource } from '@angular/common/http';
interface Product {
id: number;
name: string;
price: number;
}
interface ProductResponse {
data: Product[];
total: number;
}
@Component({
selector: 'app-product-grid',
standalone: true,
template: `
<div class="product-container">
<div class="actions">
<input
[value]="query()"
(input)="updateQuery($event)"
placeholder="Search products..."
/>
<button (click)="productsResource.reload()">Refresh 🔄</button>
</div>
@if (productsResource.isLoading()) {
<div class="skeleton">Loading products...</div>
}
@if (productsResource.error()) {
<div class="error-banner">
Failed to load products. Reason: {{ getErrorMessage() }}
</div>
}
@if (productsResource.value(); as response) {
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
@for (product of response.data; track product.id) {
<tr>
<td>{{ product.id }}</td>
<td>{{ product.name }}</td>
<td>{{ product.price | currency }}</td>
</tr>
} @empty {
<tr><td colspan="3">No products found.</td></tr>
}
</tbody>
</table>
<div class="pagination">
<button [disabled]="page() === 1" (click)="prevPage()">Prev</button>
<span>Page {{ page() }}</span>
<button
[disabled]="page() * 10 >= response.total"
(click)="nextPage()"
>
Next
</button>
</div>
}
</div>
`
})
export class ProductGridComponent {
query = signal('');
page = signal(1);
// Declarative resource
productsResource = httpResource<ProductResponse>(() => {
const search = this.query();
const currentPage = this.page();
return `/api/products?q=${search}&page=${currentPage}&limit=10`;
});
updateQuery(event: Event) {
const input = event.target as HTMLInputElement;
this.query.set(input.value);
this.page.set(1); // Reset page on new search
}
nextPage() {
this.page.update(p => p + 1);
}
prevPage() {
this.page.update(p => Math.max(1, p - 1));
}
getErrorMessage(): string {
const err = this.productsResource.error();
return err instanceof Error ? err.message : 'Unknown error';
}
}
Scenario 2: Debounced API Query with rxResource
If you want to debounce rapid keystrokes to prevent spamming the backend, we can pair rxResource with RxJS operators:
import { Component, signal } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { debounceTime, distinctUntilChanged, switchMap, of } from 'rxjs';
@Component({
selector: 'app-user-search',
standalone: true,
template: `
<div class="search-box">
<input
[value]="searchVal()"
(input)="searchVal.set($any($event.target).value)"
placeholder="Type a username..."
/>
@if (searchResource.isLoading()) {
<p>Searching database...</p>
}
@if (searchResource.value(); as users) {
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }} ({{ user.email }})</li>
} @empty {
<li>No users found.</li>
}
</ul>
}
</div>
`
})
export class UserSearchComponent {
searchVal = signal('');
searchResource = rxResource({
// Return parameters as a signal object
request: () => ({ term: this.searchVal() }),
loader: ({ request }) => {
if (!request.term.trim()) return of([]); // Return empty array immediately
// Debounce and call API
return of(request.term).pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.http.get<any[]>(`/api/users?search=${term}`))
);
}
});
constructor(private http: HttpClient) {}
}
Scenario 3: Promise-based Geolocation & Weather (resource)
This example utilizes the base resource() function to first get the user's browser Geolocation coordinates via a Promise, and then feed those coordinates into a weather API search.
import { Component, signal, computed } from '@angular/core';
import { resource, httpResource } from '@angular/core';
interface Coords {
lat: number;
lng: number;
}
@Component({
selector: 'app-local-weather',
standalone: true,
template: `
<div class="weather-card">
<h2>Local Weather</h2>
@if (locationResource.isLoading()) {
<p>Requesting GPS permissions...</p>
} @else if (locationResource.error()) {
<p>Location access denied or unavailable.</p>
}
@if (weatherResource.isLoading()) {
<p>Fetching forecast for coordinates...</p>
}
@if (weatherResource.value(); as weather) {
<div class="weather-data">
<h3>{{ weather.locationName }}</h3>
<p class="temp">{{ weather.temperature }}°C</p>
<p class="desc">{{ weather.condition }}</p>
</div>
}
</div>
`
})
export class LocalWeatherComponent {
// 1. Get Location Coordinates via Geolocation Web API Promise
locationResource = resource<Coords | undefined>({
loader: () => new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
(pos) => resolve({ lat: pos.coords.latitude, lng: pos.coords.longitude }),
(err) => reject(err),
{ timeout: 5000 }
);
})
});
// 2. Fetch Weather based on derived coordinates
weatherResource = httpResource<any>(() => {
const coords = this.locationResource.value();
if (!coords) return undefined; // Skips fetch if coordinates aren't resolved yet
return `/api/weather?lat=${coords.lat}&lng=${coords.lng}`;
});
}
Comparison Cheat Sheet 📊
| Feature | resource() | rxResource() | httpResource() |
|---|---|---|---|
| Loader Type | Returns Promise | Returns Observable | Returns URL string / Request options |
| Best Used For | Web APIs, native fetch, Promises | RxJS flows, operators, debouncing | Standard Angular REST APIs |
| Supports Interceptors | No (unless called inside fetch) | Yes (when returning HttpClient calls) | Yes (fully integrated with HttpClient) |
| Cancellation | Uses abortSignal | Automatically unsubscribes | Aborts active request |
| Dependency Context | Standard Angular | @angular/core/rxjs-interop | @angular/common/http |
Best Practices & Anti-Patterns ⚠️
❌ Don't use Resources for Mutations (POST, PUT, DELETE)
Resources are designed for read-only, side-effect-free data fetching (GET). Do not trigger state writes, database modifications, or form submissions via a resource loader. For mutations, use standard HttpClient calls inside method handlers:
// ❌ BAD: Attempting to submit a form via resource
submitForm = resource({
request: () => this.formData(),
loader: ({ request }) => this.http.post('/api/submit', request).toPromise()
});
// ✅ GOOD: Handle submission with a method
onSubmit() {
this.http.post('/api/submit', this.formData()).subscribe(() => {
this.tableResource.reload(); // Refresh table data
});
}
❌ Don't Mutate the Resource .value() Directly
The value signal exposed by a resource is read-only. If you need to make local edits to fetched data (e.g., editing a profile template before submitting), write the resource value into a writable signal or linkedSignal():
// Fetch original profile
profileResource = httpResource(() => `/api/profile`);
// Create a local writable signal that updates when resource value resolves
editableProfile = linkedSignal(() => this.profileResource.value());
✅ Clean up Resources on Component Destroy
Angular automatically cleans up resources and unsubscribes/aborts active requests when the declaring component is destroyed. However, if you run a resource inside a global Singleton Service, ensure it is cleaned up manually or designed with long-running requirements in mind.
Conclusion
The Resource API represents another massive stride toward a fully reactive, zoneless, signal-first Angular ecosystem. By packaging data fetching, loading flags, errors, status enums, and request cancellation into simple declarative functions, Angular drastically cuts down boilerplate code.
Here's your quick cheat sheet:
httpResource— Your everyday HTTP calls. Start here.rxResource— When you need RxJS operators likedebounceTimeor custom retry logic.resource— For Promise-based utilities and native Web APIs (Geolocation, Clipboard, etc.).
Once you've mastered fetching data reactively, the natural next step is handling user input reactively too. Check out our Angular Signal Forms: Zero to Hero Guide to complete the picture.
For a deep understanding of how signals track computations under the hood, revisit the foundational Angular Signals Complete Guide. Happy coding! 🚀
🧠 Test Your Knowledge
Now that you've learned the concepts, let's see if you can apply them! Take this quick quiz to test your understanding.