Angular v19.2 is here, bringing yet another set of features designed to simplify our development experience.
This time, the spotlight is on the new httpResource()
API, which joins the resource family, alongside resource()
and rxResource()
.
We previously explored these APIs in another article that you can find below:

Angular v19+ β Understanding the New resource() and rxResource() APIs π₯π
Giorgio Galassi γ» Mar 20
βHow Does It Work?
Letβs first take a look at how the rxResource()
API is typically used to perform a backend call:
import { rxResource } from "@angular/core/rxjs-interop";
rxUsers = rxResource<User[], string | undefined>({
request: () => this.query(),
loader: ({ request }) =>
this.#http.get<{ users: User[] }>(`${API_URL}/search?q=${request}`)
.pipe(
distinctUntilChanged(),
map(({ users }) => users),
catchError(() => {
throw Error('Unable to load!');
})
),
});
In this example, we still rely on HttpClient
, and the .get()
method returns an Observable
, which we process using RxJS operators
like map()
and catchError()
.
β Nothing is wrong with this approach, but what if we wanted a simpler API that integrates directly with HttpClient and eliminates the need to manually handle Observables?
π Exploring httpResource()
Since httpResource()
is built directly on top of HttpClient
, we no longer need to manually handle API requests.
Instead, we can simply pass the API URL as a parameter:
import { httpResource } from "@angular/common/http";
httpUsers = httpResource<User[]>(API_URL);
With just this single line of code, httpResource()
automatically manages the request logic, leveraging Angular's HttpClient
behind the scenes.
β
But what if we need more flexibility?
Real-world applications often require:
- Setting default values to prevent UI issues.
- Parsing API responses before assigning them to the Signal.
Letβs explore how httpResource()
handles this next.
π Using defaultValue
and parse
for Safer Data Handling
By default, httpResource() expects valid data from the API. However, in real-world scenarios, we might need to:
β
Ensure a default value when the API returns null or undefined.
β
Validate and parse responses before assigning them to the signal.
Using Zod for schema validation, we can enforce type safety directly within httpResource()
:
import { httpResource } from "@angular/common/http";
import { z } from 'zod';
// Define a schema using Zod
const UserSchema = z.object({ name: z.string(), surname: z.string() });
const UserResultsSchema = z.array(UserSchema);
type User = z.infer(typeof UserSchema);
httpUsers = httpResource<User[]>(
() => `${API_URL}/search?q=${this.query()}`,
{
defaultValue: [], // Ensures an empty array if no data is returned
parse: (data) => UserResultsSchema.parse(data) // Validates and parses the API response
}
);
β Why is this useful?
- Prevents UI errors from unexpected null values.
- Ensures type safety by validating API responses.
- Eliminates extra validation logic in the component
π Advanced Use Case
Passing just the API URL is often not enough. In our rxResource()
example, we included a search query parameter tied to the query()
signal. Letβs explore how to achieve the same behavior using the httpResource()
API:
import { httpResource } from "@angular/common/http";
httpUsers = httpResource<User[]>(() => `${API_URL}/search?q=${this.query()}`);
By passing a reactive function to httpResource()
, the API automatically listens for changes in the query
signal.
Every time query()
updates, a new request is triggered, ensuring that the latest data is always fetched.
β οΈ Important Considerations
Each new API request cancels the previous one, similar to how the switchMap
operator works in RxJS.
This is the default behavior of httpResource()
.
Be cautious when using this API for updates (PUT, POST, DELETE requests).
A subsequent update could cancel a previous request before it completes, leading to potential data loss.
β Avoid using httpResource()
for mutations
βοΈ Use it for fetching data (GET requests) instead
β³ Enabling API Calls Only When Needed
By default, httpResource()
automatically executes the API call whenever the request function changes. However, in some cases, we don't want the API to trigger immediatelyβfor example, when data fetching should only happen after a user action (e.g., clicking a button).
We can conditionally enable the request by returning undefined when it should not be executed:
import { httpResource } from "@angular/common/http";
httpUsers = httpResource<User[]>(() => {
const enabled = this.isApiEnabled();
return enabled ? `${API_URL}/search?q=${this.query()}` : undefined;
});
β Why is this useful?
- Avoids unnecessary API calls when data isnβt required.
- Helps control execution flow, ensuring the request happens only when a condition is met.
- Ideal for scenarios like lazy loading, conditional search, or toggle-based requests.
β‘ What If I Want to Use Pipeable Operators?
Everything is working as expected, but thereβs a key difference between rxResource() and httpResource().
Since httpResource()
does not expose an observable, we can't apply RxJS operators like distinctUntilChanged()
or debounceTime()
directly.
This means we need an alternative approach to manage request optimizations.
There are two possible solutions:
- Use
rxResource()
when you need to transform or process the response using RxJS operators. -
Create a utility function that allows us to bridge the gap, applying RxJS operators to Signals before passing them to
httpResource()
.
Letβs explore Approach #2 in detail.
π From Signal to Observable and Back to Signal
Angular provides interoperability functions that help bridge the gap between Signals and Observables:
-
toObservable()
β Converts aSignal
into anObservable
. -
toSignal()
β Converts anObservable
back into aSignal
.
By leveraging these utilities, we can apply RxJS operators like debounceTime()
and distinctUntilChanged()
while keeping httpResource()
reactive.
π Applying RxJS Operators to Signals
import { signal } from "@angular/core";
import { httpResource } from "@angular/common/http";
import { toObservable, toSignal } from "@angular/core/rxjs-interop";
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
inputQuery = signal<string>('');
inputQuery$ = toObservable(this.inputQuery).pipe(
distinctUntilChanged(),
debounceTime(400)
);
query = toSignal(this.inputQuery$);
httpUsers = httpResource<User[]>(() => `${API_URL}/search?q=${this.query()}`);
Here, we use a Signal to manage the input event and trigger API requests while leveraging an Observable to handle the stream flow efficiently.
Note:
This example works as expected, but since we are delaying the event and not the actual API request, the isLoading state could not reflect the truth.
π Simplifying with a Utility Function
To avoid writing this logic repeatedly, we can create a utility function that applies debouncing and distinct filtering to any Signal
:
import { Signal } from "@angular/core";
import { toObservable, toSignal } from "@angular/core/rxjs-interop";
import { distinctUntilChanged, debounceTime } from "rxjs";
export function debounceDistinctSignal<T>(signal: Signal<T>, time: number) {
const obs$ = toObservable(signal).pipe(
distinctUntilChanged(),
debounceTime(time)
);
return toSignal(obs$);
}
Now, we can simplify our original implementation like this:
import { httpResource } from "@angular/common/http";
import { debounceDistinctSignal } from 'signal-utils';
query = debounceDistinctSignal(this.inputQuery, 400);
httpUsers = httpResource<User[]>(() => `${API_URL}/search?q=${this.query()}`);
π― Try it in Action! π
Want to see httpResource()
in action? Check out this interactive StackBlitz demo, where you can:
β
Experiment with httpResource()
for API calls
β
Test a search bar with debounce & distinctUntilChanged
β
Compare side-by-side with rxResource()
and resource()
π Summary & Next Steps
Hereβs what weβve learned in this article:
httpResource()
simplifies API calls by integrating directly with HttpClient
.
toObservable()
and toSignal()
provide interoperability between Signals and RxJS.
A custom utility function (debounceDistinctSignal()
) helps clean up our code and makes this approach reusable.
Thank you for staying with me, and I hope everything was clear. Feel free to explore more of my articles and follow me on LinkedIn!
See you in the next one!
Best, G.
Top comments (0)