In a previous article, I have covered the new Resource API, which was added in Angular in version 19 as an experimental feature to promote better approaches with reactive programming. If you have not read that article and are completely unfamiliar with the Resource API completely, I suggest you either read the article or familiarize yourself with the API via the new RFCs the Angular team just published (RFC 1, RFC 2).
So, if we are on the same page now, let's explore the newest member of Angular's reactivity family: httpResource
!
What is httpResource
?
httpResource
is a reactive primitive, much like resource
/rxResource
from the previous article, but simplified and specifically tailored to work with HTTP GET requests. Why only "GET" requests, one might wonder? Well, we are going to answer this in detail later in the article, so stay tuned!
The simplest example
So, how does httpResource
look in action? Let's explore this with a simple example:
import { httpResource } from '@angular/common/http';
@Component({
template: `
@if (users.hasValue()) {
<ul>
@for (user of users.value(); track user.id) {
<li>{{ user.name }}</li>
}
</ul>
} @else if (users.isLoading()) {
<p>Loading...</p>
} @else if (users.error()) {
<button (click)="users.reload()">Retry</button>
}
`,
})
export class UserListComponent {
users = httpResource(() => 'https://jsonplaceholder.typicode.com/users');
}
As we can see, httpResource
returns a ResourceRef
just like resource
/rxResource
, with the same methods and properties like isLoading
, hasValue
, error
, etc (consult the article mentioned above or the RFCs for the full APIs). But, crucially, we write a bit less code, just calling the httpResource
function with a callback that returns a URL to which the HTTP call will be performed. But why a callback and not just a string? Let us see.
HTTP resources depending on signals
Now, let us imagine that we are building the "User Details" page, which gets the id of a user via a signal input and fetches the user data from the server. We can use the httpResource
function to achieve this:
@Component({
template: `
@if (user.hasValue()) {
<h1>{{ user.value().name }}</h1>
<p>{{ user.value().email }}</p>
} @else if (user.isLoading()) {
<p>Loading...</p>
} @else if (user.error()) {
<button (click)="user.reload()">Retry</button>
}
`,
})
export class UserDetailsComponent {
userId = input.required<number>();
user = httpResource(() => `https://jsonplaceholder.typicode.com/users/${this.userId()}`);
}
As we can see, here, we are passing the userId
signal to the httpResource
callback, which allows us to fetch the user data based on the signal value. This is powerful, because the callback for httpResource
is tracking any signals passed to it, so, if, in our example, userId
changes, the httpResource
will automatically refetch the user data for the new id from the backend. This makes httpResource
a great reactive building block, because we can use it simply in one line to create everything we need in regards to HTTP GET requests (which are usually the majority of requests in most applications).
Note: when one or more of the tracked signals change, the
httpResource
will cancel any current HTTP calls and switch to the new one, similar to howswitchMap
works in RxJS.
By the way, this is even a more powerful tool when combined with Angular's new component-routing input binding, where we can get parameters from the URL as signal inputs and use them in httpResource
. If you are unfamiliar with component input binding, check this documentation page, but for this particular example, what we need to do is enable the input binding:
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(appRoutes, withComponentInputBinding()),
]
};
And then define the parameters for our component's route:
export const appRoutes: Routes = [
{ path: 'users/:userId', component: UserDetailsComponent, },
];
This was, we can
- Use parameters from URL as if they were component inputs
- Load data based on parameters
- Keep everything in sync if the user navigates to another user's page, changing the
userId
parameter
Now, let us explore what else does this new resource have in store for us.
What else does httpResource
has to offer?
As we can see, httpResource
is working by default with JSON responses - which makes sense, since it is the well-known default for HttpClient
itself, and httpResource
is built on top of it. But what if we want to work with other types of responses, like text, blobs, or even custom types?
No worries, httpResource
has an API similar to input
signals, where we could specify a required input by doing input.required<T>()
, and we can do the same with the response type:
@Component({
template: `
@if (data.hasValue()) {
<pre>{{ data.value() }}</pre>
} @else if (data.isLoading()) {
<p>Loading...</p>
} @else if (data.error()) {
<button (click)="data.reload()">Retry</button>
}
`,
})
export class RawDataComponent {
fileId = input.required<number>();
fileResource = httpResource.blob(() => `my-api.url/files/${this.fileId()}`);
}
In this case, we would get access to the response as a blob, and can then utilize it using tools like new File
or Reader
. Not that we can also get a streamed ArrayBuffer
using httpResource.arrayBuffer
or plain text using httpResource.text
.
Parsing responses
In the modern web, runtime type validation libraries like Zod are now a crucial part of many applications. If you are unfamiliar with Zod, in short, it is a TypeScript-first schema declaration library, which allows us to do things like this:
const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof userSchema>;
Then, we can verify (at runtime!) if certain objects are compliant with a given schema, or parse an object to be assured it is of certain type:
const user = userSchema.parse({ id: 1, name: 'John Doe', email: 'johndoe@example.com'});
One of the popular use cases for Zod and similar tools is verifying HTTP responses; for instance, with Angular's HttpClient
, we pass an optional type parameter to indicate what kind of response we expect, but there is no actual way of verifying it:
this.http.get<User>('https://jsonplaceholder.typicode.com/users/1').subscribe(user => {
// user is of type User, but it is not guaranteed to *actually* be this type
// we can only hope that the server response is correct
});
Instead, many developers currently use Zod to verify the response:
this.http.get('https://jsonplaceholder.typicode.com/users/1').pipe(
map(response => userSchema.parse(response))
).subscribe(user => {
// user is of type User, and we are sure that it is correct
});
So, httpResource
makes it way easier to this refinement with Zod or any other validation library by adding a parse
option when using it:
export class UserDetailsComponent {
userId = input.required<number>();
user = httpResource<User>({
url: (id: number) => `https://jsonplaceholder.typicode.com/users/${this.userId}`,
parse: userSchema.parse,
});
}
Now, into the full request API!
Customized HTTP calls
Sometimes, we need to have a different methods for making requests, and while using "POST" to get the data is not a conventional approach, sometimes we just do not have control over the APIs that have to work with; in that case, we can invoke httpResource
with a configuration object:
export class UserDetailsComponent {
userId = input.required<number>();
user = httpResource<User>({
url: () => `https://jsonplaceholder.typicode.com/users/`,
method: 'POST',
body: { id: this.userId },
headers: { 'Content-Type': 'application/json' },
parse: userSchema.parse,
});
}
On top of this, the ResourceRef
we get from httpResource
is actually a specific HttpResourceRef
, which extends ResourceRef
with additional methods and properties. Let's see that in action now.
Reading headers
In some cases, it can be important to access the response headers of a specific HTTP request. For instance, we might want to read the Content-Type
header to determine how to parse the response. With httpResource
, we can do this easily:
@Component({
template: `
@if (data.headers().get('Content-Type') === 'application/json') {
<pre>{{ data.value() | json }}</pre>
}
`,
})
export class JsonDataComponent {
data = httpResource(() => 'my.url');
}
The headers
signal from HttpResourceRef
is an HttpHeaders
object, giving us the ability to read and check any necessary headers.
Reading status
Sometimes, the HTTP status can also be important, for example, to determine what specific UI to display in case of an error:
@Component({
template: `
@if (data.status() === 404) {
<p>Not found</p>
}
@if (data.status() === 401) {
<p>Unauthorized</p>
<button routerLink="/login">Login</button>
}
`,
})
export class StatusComponent {
data = httpResource(() => 'my.url');
}
Download progress
When downloading large responses (like Blobs) with httpResource
, sometimes it can be handy to display a progress indicator. We can do this by utilizing the HttpResourceRef.progress
signal, which returns an HttpProgressEvent
object:
@Component({
template: `
@if (data.isLoading()) {
<p>Downloading...</p>
<app-progress-bar
max="100"
[value]="data.progress().loaded / data.progress().total * 100"/>
}
`,
})
export class UploadComponent {
data = httpResource.blob(() => 'my.url');
}
Warning: using the
fetch
implementation of HTTP calls is not capable of sending progress events, so this feature is only available if you do not provide theHttpClient
with thewithFetch
option
Note: as
httpResource
is designed for retrieving data, you should only use it to download, not upload (files, blobs, etc)
Now, as we familiarized ourselves with the API and structure of httpResource
, let's see what downsides and concerns it has (at least at this initial stage).
What are the concerns?
First of all, as mentioned, httpResource
uses HttpClient
under the hood, which means that you need to provide it in the application config:
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
],
};
This is not a big deal, but if you are, for whatever reason, not using HttpClient
and utilize something different (maybe straight fetch
or some library), you will not be able to use httpResource
, and should instead rely on the resource
building block:
export class UserDetailsComponent {
userId = input.required<number>();
user = resource<User>({
request: () => ({id: this.userId()}),
loader: () => fetch(`https://jsonplaceholder.typicode.com/users/${this.userId}`),
});
}
Next, it is important to understand that httpResource
makes the requests eagerly, whenever created, meaning it can be trickier to chain requests. For instance, you might load a Product
object and then want to load other products from a "similar-products" endpoint. With httpResource
, you would need to create a new httpResource
instance that depends on the first one, and handle the chaining logic by returning undefined
while the first request is still loading and the data is yet to become available:
export class ProductComponent {
productId = input.required<number>();
product = httpResource({
url: (id: number) => `https://my-api.url/products/${this.productId()}`,
parse: productSchema.parse,
});
similarProducts = httpResource<Product[]>({
url: () => this.product.hasValue() ?
`https://my-api.url/products/${this.productId()}/similar` :
undefined,
parse: productSchema.parse,
});
}
So, with this, we covered everything we can know about httpResource
right now, but a lot of it remains to be seen. In the next section, let us explore the future of resources in Angular to understand how the grand logic of HTTP operations will be shaped.
Going forward with resources
The Angular team has made it clear that the signals will be the go-to way of working with HTTP-related operations in Angular apps in the future, and Resources are the current thing that supports a part of it. However, currently, it only covers the half of the equation: getting data from a server.
But, as we mentioned, httpResource
by default uses the "GET" method, and is designed for data retrieval only. Of course, technically, we could force it to use a different method and perform a different operation (like deleting something from the database instead of fetching).
However, philosophically, resources are not suitable for this; they are designed as writable computed signals of which the source data is something on the backend. Deleting something, for example, is not "data" and using resources to represent them is confusing.
So, what are the options? Currently, the Angular team has not settled on one particular approach, but it hasn't stopped Angular enthusiasts from exploring possibilities; let's take a look at some of them now.
For instance, Tomas Trajan has suggested a concept of crudResource
, in which all the operations related to a backend entity are encapsulated in a single resource object. Here is an example:
@Component({ /* ... */ })
export class TodoComponent {
todos = crudResource<Todo, string>('/todos', {
strategy: 'optimistic',
create: { behavior: 'concat' },
remove: { behavior: 'merge' }
});
newTodo = '';
create() {
const title = this.newTodo;
this.newTodo = '';
this.todos.create({ id: v4(), title, completed: false });
}
toggle(todo: Todo) {
this.todos.update(todo.id, {
...todo,
completed: !todo.completed
});
}
remove(todo: Todo) {
this.todos.remove(todo.id);
}
}
You can read more about it in Tomas' tweet or try it out using the code he provided in this Github Gist.
While Tomas' approach seeks to replace the httpResource
with a higher-level building block to encompass all possible operations, a solution proposed by Marko Stanimirovic seeks to expand resources with the addition fo a new httpMutation
primitive, which would cover everything related to other HTTP operations like "update" or "delete". Here is a quick look:
const updateUser = httpMutation((user: User) => ({
url: `/users/${user.id}`,
body: user,
method: 'PUT',
onSuccess: () => usersResource.reload(),
onError: console.error,
}));
// Execute mutation
updateUser.mutate({ id: 1, name: 'Marko' });
// Status
updateUser.isPending();
updateUser.isFulfilled();
updateUser.isError();
On top of this, Marko has also proposed an rxMutation
that leverages the power of RxJS to provide the full range of capabilities that might be necessary when doing such operations:
// Simple Mutation
const deleteCustomer = rxMutation((id: number) => customersService.delete(id));
deleteCustomer.execute(123);
// With Concurrency Control
const updateCustomer = rxMutation({
executor: (customer: Customer) => customersService.update(customer),
operator: concatMap, // mergeMap by default for parallel mutations
onSuccess: ({ value }) => {
console.log(`Customer ${value.name} updated successfully`);
},
onError: ({ input, error }) => {
console.error(`Failed to update ${input.name}`, error);
}
});
To better familiarize yourself with these concepts, read Marko's tweets (here and here) and give rxMutation
a try with this repository.
Warning: ideas mentioned in this section are just that: ideas proposed by curios members of the Angular community. They may or may not become a part of Angular itself, so be careful when using them in your projects.
Conclusion
Starting from v16, signals and everything else that grow out of them (linked signals, resources, etc) have become an incredibly important part of the Angular ecosystem. With the introduction of httpResource
, we are now able to handle HTTP GET requests in a more reactive and concise way, which is a great step forward in the evolution of the framework.
As mentioned, resources are still experimental, and there is lots of stuff about them that still needs to be added or ironed out. This is why I'd like to encourage readers of this article to also read the RFCs mentioned at the beginning and give feedback and new ideas so we can settle on the best approaches in the future.
Small Promotion
My book, Modern Angular, is now in print! I spent a lot of time writing about every single new Angular feature from v12-v18, including enhanced dependency injection, RxJS interop, Signals, SSR, Zoneless, and way more.
If you work with a legacy project, I believe my book will be useful to you in catching up with everything new and exciting that our favorite framework has to offer. Check it out here: https://www.manning.com/books/modern-angular
