| |

Angular 53 🅰️ The Resource API — resource() and httpResource()

The Resource API is Angular’s answer to asynchronous data in a signal-based world. Before it, the standard pattern for fetching data was a combination of toObservable, switchMap, and toSignal — a pipeline that worked but required the developer to manage loading states, errors, cancellation, and reloads manually. The resource() function wraps an async operation and exposes its result, status, and error as signals, and the httpResource() function is the purpose-built wrapper around HttpClient that handles the HTTP case. Together they turn data fetching from a manual orchestration into a declarative declaration, and the result is a component that reads its data like any other signal. This chapter covers the two functions in detail: the params and loader options, the value, status, error, and isLoading signals, the cancellation and reload behavior, the httpResource request object, and the patterns that make async data predictable.

Key point: The resource() function takes a params computation and a loader function. The params is a reactive function that produces the request parameters, and the loader is an async function that performs the operation. When the params changes, the loader re-runs with the new parameters, and the previous operation is aborted. The resource exposes value(), status(), error(), and isLoading() as signals. The status() is one of 'idle', 'loading', 'reloading', 'error', or 'resolved'. The httpResource() is built on top of HttpClient, so it supports interceptors and the existing testing utilities, and it initiates the request eagerly without a subscription. It is for reads, not for mutations.


Why the Resource API exists

Signals are synchronous. They represent a current value, and the value is read with (). Asynchronous data is the opposite: the value is not available yet, and it arrives later. The Resource API is the bridge that makes the async data available as a signal.

The problem it solves. Before the Resource API, a component that fetched data based on a signal input had to combine toObservable, switchMap, and toSignal, and manage the loading state, the error state, and the cancellation. The code worked, but it was verbose, and the loading state was a separate signal that had to be kept in sync.

The resource’s declaration. The resource() function declares the async operation once. The params declares the inputs, the loader declares the operation, and the resource exposes the result. The loading, the error, and the cancellation are managed by the resource.

The signal-based consumption. The resource’s value is a signal, and the template reads it like any other signal. The status() and the error() are the signals, and the template uses them for the loading and error states. The resource integrates with the computed, the effect, and the linkedSignal.

Why the resource is for reads. The resource is for the operations that fetch data. It cancels the in-progress operation when the parameters change, which is the right behavior for a read and the wrong behavior for a mutation. For the mutations, the HttpClient methods are the right tool .

Why the resource is the modern answer. The resource is the Angular team’s answer to the common case: load this data when this reactive parameter changes, expose the loading and the error, and update the template. The pattern is the same for every async read, and the resource is the single declaration .

Why the resource is stable. The resource API was experimental in Angular 19 and became stable in Angular 22. The httpResource is the HTTP-specific variant, and the rxResource is the RxJS-based one. The three are the stable API for the async data .

Why the resource is not the end of RxJS. The resource handles the common read case, but the RxJS remains for the complex streams, the events, and the orchestration. The rxResource is the bridge for the existing observable sources, and the toSignal and toObservable remain for the interop. The resource is the declarative answer for the data fetching, and the RxJS is the tool for the streams .


The resource() function

The resource() function creates a resource from a params computation and a loader function.

import { resource, signal } from '@angular/core';

@Component({ selector: 'app-user', standalone: true, template: `` })
export class UserComponent {
  readonly userId = signal('1');

  readonly user = resource({
    params: () => ({ id: this.userId() }),
    loader: ({ params, abortSignal }) =>
      fetch(`/api/users/${params.id}`, { signal: abortSignal })
        .then((r) => r.json() as Promise<User>),
  });
}

The params returns the request parameters, and the loader performs the operation. When the userId changes, the params changes, and the loader re-runs with the new parameters.

Why the params is reactive. The params function reads the signals, and the reads are tracked. When any of the read signals changes, the resource produces a new parameter value, and the loader re-runs. The params is the computed for the request .

Why the loader receives the abortSignal. The abortSignal is passed to the loader and aborts the previous operation when the parameters change. The fetch accepts the signal, and the request is canceled. The pattern is the cancellation, and the resource provides the signal .

Why the loader can return undefined for the params. When the params returns undefined, the loader does not run, and the status is 'idle'. The pattern is the way to disable the fetch until the required parameters are available.

readonly user = resource({
  params: () => (this.userId() ? { id: this.userId() } : undefined),
  loader: ({ params }) => fetch(`/api/users/${params.id}`).then((r) => r.json()),
});

The userId() ? ... : undefined disables the fetch when the user ID is not set. The pattern is the idiomatic way to handle the optional parameters .

Why the defaultValue option exists. The defaultValue provides the value before the first load or on error. The resource’s value() returns the default when the server value is unavailable. The option is for the templates that need a value at all times .

Why the equal option exists. The equal option provides the equality function for the loader’s return value. The resource uses the function to decide whether the value has changed, and the custom equality is for the cases where the reference equality is not enough .

Why the resource is a ResourceRef. The resource() function returns the ResourceRef<T>, which is the writable resource. The value can be set locally with .set() or .update(), and the reload() method re-runs the loader. The Resource<T> is the read-only interface, and the ResourceRef is the writable .


The resource’s signals

The resource exposes several signals. Each has a specific purpose, and the template uses them for the loading, the error, and the data.

The value() signal. The value() is the loader’s result. It is undefined until the loader completes, and it is the previous value during the reloading .

The status() signal. The status() is the resource’s state. It is one of 'idle', 'loading', 'reloading', 'error', 'resolved', or 'local'. The 'idle' means the resource has no valid request, the 'loading' means the loader is running for the first time, the 'reloading' means the loader is running again, the 'error' means the loader failed, the 'resolved' means the value is from the loader, and the 'local' means the value was set locally .

The error() signal. The error() is the loader’s error. It is undefined when there is no error, and the template uses it for the error state .

The isLoading() signal. The isLoading() is true while the loader is running. It is the shorthand for the status() === 'loading' || status() === 'reloading' .

The hasValue() method. The hasValue() is the type guard that strips the undefined from the value’s type. It is the way to access the value safely when the type includes the undefined.

if (user.hasValue()) {
  console.log(user.value().name);  // value() is User, not User | undefined
}

The hasValue() is the type guard, and the value() is the narrowed. The pattern is the safety for the template and the code .

Why the value() throws in the error state. The value() throws when the resource is in the error state. The hasValue() is the guard that prevents the access, and the template uses the status() or the hasValue() before the value(). The behavior is the safety, and the guard is the requirement .

Why the statuses matter for the template. The 'loading' and the 'reloading' are the different: the 'loading' has no value, and the 'reloading' has the previous value. The template can show the spinner for the 'loading' and keep the previous data visible for the 'reloading' .


The httpResource() function

The httpResource() is the HTTP-specific resource. It is built on top of the HttpClient, and it handles the request, the response, and the JSON parsing.

import { httpResource } from '@angular/common/http';
import { signal } from '@angular/core';

@Component({ selector: 'app-user', standalone: true, template: `` })
export class UserComponent {
  readonly userId = signal('1');

  readonly user = httpResource<User>(() => `/api/users/${this.userId()}`);
}

The httpResource takes a reactive function that returns the URL or the request object. The request is initiated eagerly, and the response is the resource’s value.

Why the request is eager. The httpResource initiates the request when the resource is created, unlike the HttpClient which requires a subscription. The eager behavior is the resource’s model, and the subscription is not needed .

Why the HttpClient is the loader. The httpResource uses the HttpClient under the hood. The interceptors, the testing utilities, and the other HttpClient features apply. The httpResource is the reactive wrapper, and the HttpClient is the mechanism .

Why the URL can be a function. The reactive function reads the signals, and the reads are tracked. When the signals change, the URL changes, and the new request is made. The previous request is canceled.

readonly user = httpResource<User>(() => `/api/users/${this.userId()}`);

The userId is the signal, and the URL is the reactive. The pattern is the same as the resource’s params.

Why the URL can be a request object. The reactive function can return a request object with the method, the headers, the parameters, and the other options.

readonly user = httpResource<User>(() => ({
  url: `/api/users/${this.userId()}`,
  method: 'GET',
  headers: { 'X-Special': 'true' },
  params: { fast: 'yes' },
  withCredentials: true,
}));

The request object is the same as the HttpClient‘s, and the httpResource uses it .

Why the parse option matters. The parse option accepts a schema validation function, and the return type is the parsed type. The httpResource uses the function to validate the response, and the type is inferred from the function.

readonly swPerson = httpResource(
  () => `https://swapi.dev/api/people/${this.id()}`,
  { parse: starWarsPersonSchema.parse },
);

The starWarsPersonSchema.parse is the Zod parser, and the resource’s type is the parsed type. The pattern is the schema validation, and the type safety .

Why the response type methods exist. The httpResource.text(), httpResource.blob(), and httpResource.arrayBuffer() are the methods for the non-JSON responses.

readonly text = httpResource.text(() => '/api/text');
readonly blob = httpResource.blob(() => '/api/image');
readonly buffer = httpResource.arrayBuffer(() => '/api/file');

The methods are the typed, and the value is the specific type .

Why the httpResource has the extra signals. The headers(), the statusCode(), and the progress() are the HTTP-specific signals. The headers() is the response headers, the statusCode() is the HTTP status, and the progress() is the progress event .

Why the httpResource is not for the mutations. The httpResource supports the any method, but it is for the reads. The mutations should use the HttpClient methods. The resource cancels the in-progress request, which is the wrong behavior for the mutations .


The cancellation and the reload

The resource cancels the in-progress operation when the parameters change. The reload() method re-runs the loader.

The cancellation. When the params changes while the loader is running, the resource aborts the previous operation. The abortSignal is the mechanism, and the loader can use it to cancel the fetch.

loader: ({ params, abortSignal }) =>
  fetch(`/api/users/${params.id}`, { signal: abortSignal }).then((r) => r.json()),

The abortSignal is passed to the fetch, and the request is canceled when the resource aborts .

Why the cancellation matters. The cancellation prevents the stale results from the previous request. The new request’s result is the one that is used, and the previous is discarded. The pattern is the same as the switchMap‘s cancellation .

The reload() method. The reload() method re-runs the loader with the current parameters. The method is for the manual refresh, and the pattern is the refresh button.

<button (click)="user.reload()">Refresh</button>

The reload() is the imperative, and the resource re-runs the loader .

Why the reload is the same parameters. The reload() re-runs the loader with the current params, not the new. The method is for the refresh, and the parameters are the same. The status is the 'reloading', and the previous value remains visible .

Why the resource is the read. The resource’s cancellation and reload are the read semantics. The mutation should use the HttpClient, which does not cancel. The distinction is the design, and the resource is the read .


The status and the template

The template uses the status(), the isLoading(), the error(), and the hasValue() to render the different states.

The loading state. The isLoading() is the shorthand, and the status() === 'loading' is the explicit. The template shows the spinner.

@if (user.isLoading()) {
  <p>Loading...</p>
}

The error state. The error() is the error, and the template shows the message.

@if (user.error()) {
  <p>Error: {{ user.error() }}</p>
  <button (click)="user.reload()">Retry</button>
}

The data state. The hasValue() is the guard, and the value() is the data.

@if (user.hasValue()) {
  <div>{{ user.value().name }}</div>
}

Why the order matters. The template checks the error first, then the loading, then the value. The order ensures the correct state is shown, and the hasValue() prevents the value()‘s throw .

Why the status() is the explicit. The status() is the string, and the template can use the specific state. The isLoading() is the shorthand for the loading and the reloading, and the status() is the explicit for the cases where the distinction matters .


Complete Example Session

import { Component, signal, computed } from '@angular/core';
import { resource, httpResource } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';

// ============================================
// PART 1: THE BASIC RESOURCE
// ============================================

@Component({ selector: 'app-user', standalone: true, template: `` })
export class UserComponent {
  readonly userId = signal('1');

  readonly user = resource({
    params: () => ({ id: this.userId() }),
    loader: ({ params, abortSignal }) =>
      fetch(`/api/users/${params.id}`, { signal: abortSignal })
        .then((r) => r.json() as Promise<User>),
  });
}

// ============================================
// PART 2: THE HTTP RESOURCE
// ============================================

@Component({ selector: 'app-profile', standalone: true, template: `` })
export class ProfileComponent {
  readonly userId = signal('1');

  readonly user = httpResource<User>(() => `/api/users/${this.userId()}`);
}

// ============================================
// PART 3: THE REQUEST OBJECT
// ============================================

@Component({ selector: 'app-search', standalone: true, template: `` })
export class SearchComponent {
  readonly term = signal('');

  readonly results = httpResource<Result[]>(() => ({
    url: '/api/search',
    params: { q: this.term() },
  }));
}

// ============================================
// PART 4: THE OPTIONAL PARAMS
// ============================================

@Component({ selector: 'app-optional', standalone: true, template: `` })
export class OptionalComponent {
  readonly userId = signal<string | null>(null);

  readonly user = resource({
    params: () => (this.userId() ? { id: this.userId() } : undefined),
    loader: ({ params }) => fetch(`/api/users/${params.id}`).then((r) => r.json()),
  });
}

// ============================================
// PART 5: THE PARSE OPTION
// ============================================

@Component({ selector: 'app-validated', standalone: true, template: `` })
export class ValidatedComponent {
  readonly id = signal(1);

  readonly person = httpResource(
    () => `https://swapi.dev/api/people/${this.id()}`,
    { parse: starWarsPersonSchema.parse },
  );
}

// ============================================
// PART 6: THE TEMPLATE
// ============================================

@Component({
  selector: 'app-view',
  standalone: true,
  template: `
    @if (user.isLoading()) {
      <p>Loading...</p>
    } @else if (user.error()) {
      <p>Error: {{ user.error() }}</p>
      <button (click)="user.reload()">Retry</button>
    } @else if (user.hasValue()) {
      <div>{{ user.value().name }}</div>
    }
  `,
})
export class ViewComponent {
  readonly user = httpResource<User>(() => `/api/users/1`);
}

// ============================================
// PART 7: THE COMPUTED ON THE RESOURCE
// ============================================

@Component({ selector: 'app-count', standalone: true, template: `` })
export class CountComponent {
  readonly users = httpResource<User[]>(() => '/api/users');

  readonly count = computed(() => this.users.value()?.length ?? 0);
}

// ============================================
// PART 8: THE LINKED SIGNAL ON THE RESOURCE
// ============================================

@Component({ selector: 'app-history', standalone: true, template: `` })
export class HistoryComponent {
  readonly messages = httpResource<Message[]>(() => '/api/messages');

  readonly history = linkedSignal(() => this.messages.value() ?? []);
}

// ============================================
// PART 9: THE RELOAD
// ============================================

@Component({ selector: 'app-refresh', standalone: true, template: `` })
export class RefreshComponent {
  readonly data = httpResource<Data>(() => '/api/data');

  refresh(): void {
    this.data.reload();
  }
}

// ============================================
// PART 10: WHAT NOT TO DO
// ============================================

// Don't use the httpResource for the mutations
// httpResource(() => ({ url: '/api/save', method: 'POST', body }))  // ⚠️

// Don't forget the hasValue guard
// user.value().name  // ❌ throws when the error

// Don't use the resource for the synchronous data
// resource({ loader: () => 42 });  // unnecessary

// Don't forget the abortSignal in the loader
// loader: ({ params }) => fetch(...)  // the cancellation is lost

// Don't use the httpResource without the HttpClient provider
// provideHttpClient() is required

// Don't expect the resource to fetch without the params
// The params is the reactive trigger.

The ten parts cover the basic resource, the HTTP resource, the request object, the optional params, the parse option, the template, the computed, the linked signal, the reload, and the anti-patterns.


Quick Reference

The resource() Options

OptionPurpose
paramsThe reactive request parameters
loaderThe async operation
defaultValueThe value before the load
equalThe equality function

The Resource Signals

SignalPurpose
value()The loader’s result
status()The state
error()The error
isLoading()The loading flag
hasValue()The type guard

The Statuses

StatusMeaning
'idle'No valid request
'loading'First load
'reloading'Reload with the previous value
'error'The loader failed
'resolved'The value is from the loader
'local'The value is set locally

The httpResource() Forms

FormPurpose
httpResource<T>(() => url)The URL
httpResource<T>(() => ({ url, ... }))The request object
httpResource.text(() => url)The text response
httpResource.blob(() => url)The blob response
httpResource.arrayBuffer(() => url)The buffer response

The httpResource() Signals

SignalPurpose
value()The response body
status()The state
error()The error
isLoading()The loading flag
headers()The response headers
statusCode()The HTTP status
progress()The progress

The Methods

MethodPurpose
reload()Re-run the loader
set(value)Set the value locally
update(fn)Update the value locally
destroy()Destroy the resource

Best Practices

✅ Do This:

// Use the resource for the async data
readonly user = resource({ params, loader });                  // ✅

// Use the httpResource for the HTTP
readonly user = httpResource<User>(() => `/api/users/${this.userId()}`); // ✅

// Use the abortSignal in the loader
loader: ({ params, abortSignal }) => fetch(url, { signal: abortSignal }); // ✅

// Use the hasValue guard
@if (user.hasValue()) { {{ user.value().name }} }              // ✅

// Use the optional params
params: () => (this.userId() ? { id: this.userId() } : undefined) // ✅

// Use the parse option for the validation
httpResource(() => url, { parse: schema.parse });              // ✅

// Use the reload for the refresh
<button (click)="user.reload()">Retry</button>                 // ✅

// Use the status for the specific states
@if (user.status() === 'reloading') { ... }                    // ✅

❌ Don’t Do This:

// Don't use the resource for the mutations
resource({ loader: () => http.post('/api/save', data) });      // ⚠️

// Don't forget the hasValue guard
user.value().name  // ❌ throws in the error state              // ⚠️

// Don't use the resource for the synchronous data
resource({ loader: () => 42 });                                // ⚠️

// Don't forget the abortSignal
loader: ({ params }) => fetch(url);  // the cancellation is lost // ⚠️

// Don't use the httpResource without the provider
// provideHttpClient() is required                             // ⚠️

// Don't expect the fetch without the params
resource({ loader: () => fetch('/api') });  // the params is the trigger // ⚠️

Common Pitfalls

PitfallProblemSolution
The value() without the guardThrows in the errorUse hasValue()
The missing abortSignalNo cancellationPass the signal
The mutation with the resourceThe wrong toolUse HttpClient
The missing provideHttpClientThe compile errorAdd the provider
The params not reactiveNo re-fetchUse the signals
The status confusionThe wrong templateUse the specific status
The resource for the syncThe unnecessaryUse a signal

Real-World Examples

1. The basic resource

readonly user = resource({ params, loader });

2. The HTTP resource

readonly user = httpResource<User>(() => `/api/users/${this.userId()}`);

3. The optional params

params: () => (this.userId() ? { id: this.userId() } : undefined)

4. The abortSignal

loader: ({ params, abortSignal }) => fetch(url, { signal: abortSignal });

5. The parse option

httpResource(() => url, { parse: schema.parse });

6. The template

@if (user.isLoading()) { <p>Loading...</p> }

7. The error state

@if (user.error()) { <p>{{ user.error() }}</p> }

8. The computed

readonly count = computed(() => this.users.value()?.length ?? 0);

9. The reload

user.reload();

10. The linked signal

readonly history = linkedSignal(() => this.messages.value() ?? []);

Visual: The Resource

┌──────────────────────────────────────────────────────────┐
│  params: () => ({ id: this.userId() })                   │
│       │                                                  │
│       │  the reactive parameters                         │
│       ▼                                                  │
│  loader: ({ params, abortSignal }) => fetch(...)         │
│       │                                                  │
│       │  the async operation                             │
│       ▼                                                  │
│  value()    → the result                                 │
│  status()   → 'idle' | 'loading' | 'reloading' | ...     │
│  error()    → the error                                  │
│  isLoading() → the loading flag                          │
│  hasValue() → the type guard                             │
│                                                          │
│  The params change → the loader re-runs → the previous   │
│  request is aborted.                                     │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Statuses

┌──────────────────────────────────────────────────────────┐
│  'idle'       No valid request. value() is undefined.    │
│  'loading'    The first load. value() is undefined.      │
│  'reloading'  The reload. value() is the previous.       │
│  'error'      The loader failed. value() throws.         │
│  'resolved'   The value is from the loader.              │
│  'local'      The value is set locally.                  │
│                                                          │
│  The 'loading' and the 'reloading' are the different:    │
│    loading:   no value                                   │
│    reloading: the previous value                         │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The httpResource

┌──────────────────────────────────────────────────────────┐
│  httpResource<User>(() => `/api/users/${this.userId()}`)│
│       │                                                  │
│       │  the reactive URL                                │
│       ▼                                                  │
│  The request is initiated eagerly.                       │
│       │                                                  │
│       │  the HttpClient is the loader                    │
│       │  the interceptors apply                          │
│       ▼                                                  │
│  value()      → the parsed JSON                          │
│  status()     → the state                                │
│  error()      → the error                                │
│  headers()    → the response headers                     │
│  statusCode() → the HTTP status                          │
│                                                          │
│  The subscription is not needed.                         │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Template States

┌──────────────────────────────────────────────────────────┐
│  @if (user.isLoading()) {                                │
│    <p>Loading...</p>                                     │
│  } @else if (user.error()) {                             │
│    <p>Error: {{ user.error() }}</p>                      │
│    <button (click)="user.reload()">Retry</button>        │
│  } @else if (user.hasValue()) {                          │
│    <div>{{ user.value().name }}</div>                    │
│  }                                                       │
│                                                          │
│  The order: the error, the loading, the value.           │
│  The hasValue prevents the value()'s throw.              │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Cancellation

┌──────────────────────────────────────────────────────────┐
│  userId.set('1')                                         │
│       │                                                  │
│       ▼                                                  │
│  The loader runs for the user 1.                         │
│       │                                                  │
│  userId.set('2')                                         │
│       │                                                  │
│       ▼                                                  │
│  The previous request is aborted.                        │
│  The new request runs for the user 2.                    │
│       │                                                  │
│       ▼                                                  │
│  The result is the user 2.                               │
│                                                          │
│  The abortSignal is the mechanism.                       │
│                                                          │
└──────────────────────────────────────────────────────────┘

Summary

ItemValue
resource()The generic async resource
httpResource()The HTTP-specific resource
paramsThe reactive request parameters
loaderThe async operation
value()The result
status()The state
error()The error
isLoading()The loading flag
hasValue()The type guard
reload()The re-run
The cancellationThe abortSignal

Key takeaways:

  • The resource() function wraps an async operation — the params produces the request parameters, the loader performs the operation, and the result is the signals
  • The httpResource() is built on the HttpClient — it supports the interceptors, the testing utilities, and the other HttpClient features, and it initiates the request eagerly
  • The resource exposes the value(), the status(), the error(), and the isLoading() — the hasValue() is the type guard that prevents the value()‘s throw in the error state
  • The statuses are the specific states — the 'loading' has no value, the 'reloading' has the previous value, and the template uses the specific status for the correct rendering
  • The params returning undefined disables the fetch — the pattern is the idiomatic way to handle the optional parameters
  • The abortSignal is the cancellation — the loader passes it to the fetch, and the previous request is canceled when the parameters change
  • The httpResource supports the parse option for the schema validation — the Zod or the Valibot parser validates the response, and the resource’s type is inferred from the parser
  • The resource is for the reads, not the mutations — the cancellation and the reload are the read semantics, and the mutations should use the HttpClient methods
  • The reload() method re-runs the loader — the method is for the manual refresh, and the pattern is the refresh button
  • The resource integrates with the computed and the linkedSignal — the value() is a signal, and the derived values are the computed

Remember: The Resource API is the Angular’s answer to the asynchronous data in the signal-based world. The resource() function wraps the generic async operation, and the httpResource() is the HTTP-specific variant. The params and the loader declare the operation, the value(), the status(), the error(), and the isLoading() expose the result, and the abortSignal handles the cancellation. The hasValue() is the guard, the reload() is the refresh, and the parse is the validation. The resource is the declarative, and the template is the consumption.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!