State

Meet HTTP Resource

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 eith

Meet HTTP Resource — State article by Armen Vardanyan on Angular In Depth
Meet HTTP Resource — State article by Armen Vardanyan on Angular In Depth
On this page · 13 sections

In a previous article, I explored the new Resource API that arrived in Angular 19 as an experimental feature, designed to encourage better reactive programming practices. If you haven't read that piece or you're new to the Resource API entirely, I'd recommend either checking it out or getting familiar with the API through the recently published RFCs from the Angular team (RFC 1, RFC 2).

Now that we're aligned, let's dive into the latest addition to Angular's reactivity toolkit: httpResource!

Understanding httpResource

httpResource is a reactive primitive, similar to resource and rxResource from the earlier discussion, but streamlined and purpose-built for HTTP GET requests. You might wonder why only GET is supported. We'll tackle that question in depth shortly, so bear with me!

A basic illustration

What does httpResource look like in practice? Let's start with a straightforward 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 you can see, httpResource returns a ResourceRef just like its siblings resource and rxResource, complete with the same set of properties and methods such as isLoading, hasValue, error, and more (refer to the earlier article or the RFCs for the complete API surface). The key difference is that we write less boilerplate—just invoke the httpResource function with a callback that supplies a URL for the HTTP request. But why a callback rather than a plain string? Let's investigate.

Signals driving HTTP resources

Imagine we're crafting a "User Details" page that receives a user's id through a signal input and retrieves the corresponding user data from the server. Here's how httpResource can facilitate 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()}`);
}

In this scenario, we're passing the userId signal into the httpResource callback, enabling us to fetch data based on the signal's value. This is incredibly potent because the callback tracks any signals it receives. So, if userId changes in our example, httpResource automatically refetches the user data for the new id from the backend. This makes httpResource an excellent reactive building block—just one line creates everything we need for HTTP GET requests, which typically dominate most applications.

Note: when any tracked signal changes, httpResource cancels the current HTTP request and initiates a new one, akin to how switchMap operates in RxJS.

Incidentally, this becomes even more powerful when paired with Angular's component-router input binding, allowing URL parameters to arrive as signal inputs that httpResource can consume. If you're not familiar with component input binding, refer to this documentation page. For our purposes, we first enable input binding:

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(appRoutes, withComponentInputBinding()),
  ]
};

Then, we set up the route parameters for our component:

export const appRoutes: Routes = [
  { path: 'users/:userId', component: UserDetailsComponent, },
];

This approach allows us to:

  1. Treat URL parameters like component inputs
  2. Fetch data based on those parameters
  3. Keep everything synchronized when navigating to a different user's page, thus changing the userId parameter

Now, let's see what other capabilities this new resource brings.

Additional features of httpResource

By default, httpResource works with JSON responses—a sensible choice given that's the standard for HttpClient, upon which it's built. But what if we need to handle other response types, such as text, blobs, or custom formats?

Don't fret—httpResource offers an API reminiscent of input signals, where you can specify a required input via input.required<T>(). The same principle applies to response types:

@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()}`);
}

Here, we'd receive the response as a blob, which we can then process using utilities like new File or Reader. We can also obtain a streamed ArrayBuffer via httpResource.arrayBuffer or plain text through httpResource.text.

Handling response parsing

In today's web landscape, runtime type validation libraries like Zod have become indispensable in many projects. For those unfamiliar, Zod is a TypeScript-first schema declaration library that enables patterns like this:

const userSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});

type User = z.infer<typeof userSchema>;

We can then validate at runtime whether objects conform to a given schema, or parse objects to ensure they're of a certain type:

const user = userSchema.parse({ id: 1, name: 'John Doe', email: 'johndoe@example.com'});

A common use case for Zod and similar tools is verifying HTTP responses. With Angular's HttpClient, for instance, you can pass an optional type parameter to indicate the response type you anticipate, but there's no real runtime verification:

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 turn to Zod to validate responses:

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 simplifies this process with Zod or any other validation framework by offering a parse option:

export class UserDetailsComponent {
  userId = input.required<number>();
  user = httpResource<User>({
    url: (id: number) => `https://jsonplaceholder.typicode.com/users/${this.userId}`,
    parse: userSchema.parse,
  });
}

Now, let's delve into the full request API!

Tailored HTTP requests

There are times when we need alternative methods for making requests. While using "POST" to retrieve data is unconventional, sometimes we have no say over the APIs we must integrate with. In such cases, 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,
  });
}

Additionally, the ResourceRef returned by httpResource is actually a specialized HttpResourceRef, which extends ResourceRef with extra methods and properties. Let's see this in action.

Accessing headers

In some situations, reading the response headers of an HTTP request is critical. For example, we might need to inspect the Content-Type header to decide how to parse the response. With httpResource, this is straightforward:

@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, allowing us to read and validate any necessary headers.

Monitoring status

The HTTP status code can also be vital, such as determining which UI to show when an error occurs:

@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');
}

Tracking download progress

When downloading large responses (like Blobs) with httpResource, displaying a progress indicator can enhance the user experience. This is achievable through the HttpResourceRef.progress signal, which yields 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 for HTTP calls cannot emit progress events, so this feature is available only if you do not configure HttpClient with the withFetch option

Note: since httpResource is intended for data retrieval, use it exclusively for downloads, not uploads (files, blobs, etc.)

Now that we've covered the API and structure of httpResource, let's examine its potential drawbacks and concerns, at least in its current form.

Potential concerns

First, as noted earlier, httpResource relies on HttpClient internally, so it must be provided in the application configuration:

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(),
  ],
};

This is relatively minor, but if you're not using HttpClient for some reason—perhaps opting for plain fetch or another library—then httpResource isn't an option. In that case, you'd want to use the resource primitive instead:

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's worth noting that httpResource fires requests eagerly upon creation, which can complicate request chaining. For instance, you might load a Product and then want to retrieve related products from a "similar-products" endpoint. With httpResource, you'd need to create another httpResource instance that depends on the first one, managing the chaining logic by returning undefined while the initial request is pending and data hasn't arrived yet:

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,
  });
}

With that, we've covered everything currently known about httpResource, though much remains to be discovered. In the following section, let's explore the future of resources in Angular to see how the broader strategy for HTTP operations will evolve.

The road ahead for resources

The Angular team has signaled that signals will become the primary mechanism for handling HTTP-related operations in Angular applications moving forward, and Resources currently support part of that vision. However, at this stage, they only address half the equation: retrieving data from a server.

As mentioned, httpResource defaults to the "GET" method and is designed solely for data fetching. Technically, we could coerce it into using different methods for different operations (like deleting a database entry instead of fetching).

Philosophically, though, resources aren't a fitting match for such tasks. They're meant to be writable computed signals whose source is backend data. Deleting something, for example, isn't "data," and representing it via resources would be confusing.

So, what are the alternatives? The Angular team hasn't committed to a specific approach yet, but that hasn't stopped Angular enthusiasts from speculating. Let's review some of these proposals.

For example, Tomas Trajan has proposed a crudResource concept, where all backend entity operations are consolidated into a single resource object. Here's a taste:

@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 dive deeper in Tomas' tweet or experiment with the code he shared in this GitHub Gist.

While Tomas' idea aims to replace httpResource with a higher-level abstraction for all operations, a suggestion from Marko Stanimirovic seeks to enrich resources by adding a fresh httpMutation primitive, covering other HTTP operations like "update" or "delete." Here's a brief glimpse:

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();

Additionally, Marko has proposed an rxMutation that harnesses RxJS to deliver a complete suite of capabilities needed for 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 get fully acquainted with these concepts, review Marko's tweets (here and here) and give rxMutation a spin with this repository.

Warning: the ideas presented in this section are merely proposals from curious Angular community members. They may or may not be adopted into Angular itself, so be cautious about using them in your projects.

Wrapping Up

Since v16, signals and the features built on top of them—such as linked signals and resources—have become a central piece of Angular's architecture. The arrival of httpResource builds on this momentum, letting developers manage HTTP GET requests with a more reactive and streamlined syntax. It's a meaningful improvement in how the framework handles server communication.

That said, resources are still in the experimental phase, and a number of details and edge cases are yet to be finalized. I'd strongly suggest that anyone following along also takes a look at the RFCs linked earlier in this discussion. Providing feedback and sharing ideas is exactly how we'll land on the best design choices moving forward.

Shameless Plug

Gg2RPJKWwAAHSId.png
There's also good news on the book front: Modern Angular is now available in print! I put a lot of effort into documenting every fresh Angular feature between v12 and v18, covering topics like dependency injection upgrades, RxJS integration, Signals, SSR, Zoneless, and numerous others.

If you're maintaining a legacy codebase, this book should help you bridge the gap and get comfortable with all the recent additions to the framework. You can find it here: https://www.manning.com/books/modern-angular


Meet HTTP Resource — figure 2

Tagged in:

Articles

Last Update: April 14, 2025

AV
Armen Vardanyan

Writes about RxJS, State, Dependency Injection. Active 2019–2026.

All 57 articles →