Angular Resource API and httpResource: Complete Guide (Stable in Angular 22)
Angular's Resource API and httpResource are now stable as of Angular 22. This guide covers resource(), rxResource(), httpResource(), the new chain() helper for dependent resources, Zod validation, and common interview questions.

Angular's Resource API and httpResource provide signal-based data fetching that tracks loading, error, and resolved states automatically. Introduced in Angular 20, these APIs became stable in Angular 22, replacing verbose HttpClient subscription patterns with reactive primitives.
The Resource API (resource(), rxResource(), httpResource()) graduated from experimental to stable in Angular 22 (June 2026). The params and stream naming, string literal statuses, and the new chain() helper for dependent resources are all production-ready. If you're upgrading from Angular 20 or 21, the API surface remains compatible.
The Three Resource Variants
Angular offers three ways to load asynchronous data as signals. Each targets a different use case, but all share the same reactive model: declare dependencies, define a loader, and consume the result through signals.
resource()works with Promises. Ideal when usingfetch()or any promise-based API.rxResource()works with Observables. The right choice when RxJS operators likedebounceTime,retry, orswitchMapare needed.httpResource()wraps Angular'sHttpClientdirectly. Interceptors, testing utilities, and schema validation work without extra wiring.
The key difference between httpResource and the other two: httpResource uses HttpClient under the hood, which means existing interceptors continue to work. The original resource() API bypassed HttpClient entirely, which was a limitation in Angular 19.
Building a User Profile with resource()
The resource() function accepts a params computation and a loader function. When signals inside params change, the loader re-executes automatically.
import { Component, signal, resource } from '@angular/core';
interface User {
id: number;
name: string;
email: string;
}
@Component({
selector: 'app-user-profile',
template: \`
@if (userResource.hasValue()) {
<h2>{{ userResource.value().name }}</h2>
<p>{{ userResource.value().email }}</p>
} @else if (userResource.isLoading()) {
<p>Loading profile...</p>
} @else if (userResource.error()) {
<p>Failed to load user</p>
}
\`,
})
export class UserProfileComponent {
userId = signal(1);
// params produces the reactive dependency
// loader receives it and returns a Promise
userResource = resource<User, number>({
params: () => this.userId(),
loader: async ({ params: id, abortSignal }) => {
const res = await fetch(\`/api/users/\${id}\`, { signal: abortSignal });
return res.json();
},
});
loadUser(id: number) {
this.userId.set(id); // triggers automatic refetch
}
}The abortSignal parameter allows Angular to cancel in-flight requests when userId changes before the previous request completes. This prevents race conditions without manual subscription management.
Reactive Data Fetching with httpResource
httpResource eliminates boilerplate by combining URL declaration and HTTP execution in a single call. It returns an HttpResourceRef that exposes value, isLoading, error, status, and headers as signals.
import { Component, signal, computed } from '@angular/core';
import { httpResource } from '@angular/common/http';
interface Product {
id: number;
name: string;
price: number;
category: string;
}
@Component({
selector: 'app-product-list',
template: \`
@if (products.hasValue()) {
@for (product of products.value(); track product.id) {
<div class="product-card">
<h3>{{ product.name }}</h3>
<span>{{ product.price | currency }}</span>
</div>
}
} @else if (products.isLoading()) {
<p>Loading products...</p>
}
\`,
})
export class ProductListComponent {
category = signal('electronics');
// httpResource re-fetches whenever category() changes
products = httpResource<Product[]>(() => ({
url: '/api/products',
params: { category: this.category() },
}));
filterByCategory(cat: string) {
this.category.set(cat); // pending request is cancelled, new one starts
}
}The function passed to httpResource returns a request configuration object. Angular tracks signal reads inside this function, so changing category triggers a new GET request. If a request is already in flight, Angular cancels it before starting the new one.
httpResource is designed for data fetching (GET requests). Using it for POST, PUT, or DELETE operations is unsafe because request cancellation could abort mutations mid-flight. For write operations, use HttpClient directly or wrap mutations in a service method.
Chaining Resources with chain()
Angular 22 introduced the chain() helper for dependent resource loading. When one resource depends on another's value, chain() propagates loading and error states automatically.
import { Component, signal } from '@angular/core';
import { httpResource } from '@angular/common/http';
interface User {
id: number;
name: string;
authorId: string;
}
interface Post {
id: number;
title: string;
content: string;
}
@Component({
selector: 'app-user-posts',
template: \`
@if (postsResource.isLoading()) {
<p>Loading...</p>
} @else if (postsResource.hasValue()) {
<h2>Posts by {{ userResource.value().name }}</h2>
@for (post of postsResource.value(); track post.id) {
<article>
<h3>{{ post.title }}</h3>
</article>
}
} @else if (postsResource.error()) {
<p>Failed to load posts</p>
}
\`,
})
export class UserPostsComponent {
userId = signal(1);
userResource = httpResource<User>(() => \`/api/users/\${this.userId()}\`);
// chain() propagates userResource's status to postsResource
postsResource = httpResource<Post[]>({
params: ({ chain }) => {
const user = chain(this.userResource);
return { url: '/api/posts', params: { authorId: user.authorId } };
},
});
}chain(userResource) does not just return the value. While userResource is loading, postsResource also reports as loading and its loader does not execute. If userResource errors, postsResource transitions to error state with a ResourceDependencyError that carries the upstream error.
This replaces the RxJS pattern of nested switchMap calls with explicit, correct-by-default status propagation.
Schema Validation with Zod and httpResource
API responses from external services can drift from expected shapes. The parse option on httpResource integrates schema validation libraries like Zod to catch mismatches at runtime instead of silently propagating corrupt data.
import { Component, signal } from '@angular/core';
import { httpResource } from '@angular/common/http';
import { z } from 'zod';
// Define the expected shape with Zod
const OrderSchema = z.object({
id: z.number(),
status: z.enum(['pending', 'shipped', 'delivered', 'cancelled']),
total: z.number().positive(),
items: z.array(z.object({
productId: z.number(),
quantity: z.number().int().positive(),
unitPrice: z.number().positive(),
})),
createdAt: z.string().datetime(),
});
type Order = z.infer<typeof OrderSchema>;
@Component({
selector: 'app-order',
template: \`
@if (order.hasValue()) {
<h2>Order #{{ order.value().id }}</h2>
<p>Status: {{ order.value().status }}</p>
<p>Total: {{ order.value().total | currency }}</p>
} @else if (order.error()) {
<p>Invalid order data received</p>
}
\`,
})
export class OrderComponent {
orderId = signal(42);
// parse validates the response before exposing it as a signal
order = httpResource<Order>(
() => \`/api/orders/\${this.orderId()}\`,
{ parse: OrderSchema.parse }
);
}When the API returns data that doesn't match OrderSchema, the resource transitions to the 'error' status. The parse function's return type also determines the TypeScript type of value(), so schema definitions serve double duty as runtime validators and type generators.
rxResource with Stream and Params
Angular renames loader to stream and request to params in rxResource. This aligns the naming with the streaming semantics that rxResource supports. The stream function receives an Observable context and must return an Observable.
import { Component, signal } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { inject } from '@angular/core';
import { debounceTime, switchMap } from 'rxjs';
interface SearchResult {
id: number;
title: string;
excerpt: string;
}
@Component({
selector: 'app-search',
template: \`
<input (input)="query.set((.target).value)" placeholder="Search..." />
@if (results.isLoading()) {
<p>Searching...</p>
}
@if (results.hasValue()) {
@for (item of results.value(); track item.id) {
<div>{{ item.title }}</div>
}
}
\`,
})
export class SearchComponent {
private http = inject(HttpClient);
query = signal('');
// params (was "request") provides the reactive input
// stream (was "loader") returns an Observable
results = rxResource<SearchResult[], string>({
params: () => this.query(),
stream: ({ params: q }) =>
this.http.get<SearchResult[]>('/api/search', {
params: { q },
}),
});
}Unlike httpResource, rxResource gives full control over the Observable pipeline. Operators like debounceTime or retry can be chained inside stream. However, httpResource handles the most common case (single GET request) with less code.
Ready to ace your Angular interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Status Tracking: String Literals Replace Enums
ResourceStatus is a string union type. The six possible values provide granular insight into the resource lifecycle:
| Status | Meaning |
|---|---|
'idle' | params returned undefined, no request issued |
'loading' | First request in progress |
'reloading' | Subsequent request after a previous success |
'resolved' | Data available in value() |
'error' | Request failed, error() contains the error |
'local' | Value set locally via .set() or .update() |
import { Component, signal, resource } from '@angular/core';
@Component({
selector: 'app-status-demo',
template: \`
<p>Status: {{ data.status() }}</p>
@switch (data.status()) {
@case ('loading') { <spinner /> }
@case ('reloading') { <subtle-spinner /> }
@case ('resolved') { <data-table [rows]="data.value()" /> }
@case ('error') { <error-banner [error]="data.error()" /> }
@case ('idle') { <p>Select a filter to load data</p> }
}
\`,
})
export class StatusDemoComponent {
filter = signal<string | undefined>(undefined);
data = resource({
params: () => this.filter(),
loader: async ({ params: f, abortSignal }) => {
const res = await fetch(\`/api/data?filter=\${f}\`, { signal: abortSignal });
return res.json();
},
});
}Returning undefined from params sets the status to 'idle' and prevents the loader from executing. This pattern works well for conditional data fetching: display a prompt until the user provides input, then load data.
Calling value() on a resource in 'error' status throws a runtime exception. Always guard reads with hasValue() or check status() before accessing value(). This is a change from Angular 19 where value() returned undefined on error.
Angular httpResource Interview Questions
The Resource API is a standard topic in Angular interview questions. Here are questions that test real understanding beyond surface-level familiarity with the API.
Q: What problem does httpResource solve that resource() does not?
resource() uses fetch() or any promise-based loader, bypassing Angular's HttpClient. This means interceptors (for auth tokens, logging, error handling) do not apply. httpResource uses HttpClient internally, so interceptors, testing utilities (HttpTestingController), and features like withFetch() work without additional configuration.
Q: When should rxResource be preferred over httpResource?
rxResource provides full Observable control through its stream function. Choose it when the data pipeline requires RxJS operators: debouncing search input, retrying failed requests with exponential backoff, or combining multiple streams with combineLatest. For straightforward GET requests, httpResource requires less code.
Q: How does Angular handle concurrent requests when a signal changes rapidly?
All three resource variants cancel pending requests when params produces a new value. For httpResource and resource(), the AbortSignal cancels the underlying fetch. For rxResource, Angular unsubscribes from the previous Observable. This prevents stale responses from overwriting fresh data.
Q: What is the purpose of the 'local' status?
Calling .set() or .update() on a resource changes its value locally without triggering the loader. The status transitions to 'local', indicating the current value did not come from the server. This supports optimistic UI updates: the UI reflects the change immediately while a separate mutation request runs.
Q: How does Zod integration work with httpResource?
The parse option accepts any function with the signature (data: unknown) => T. When the HTTP response arrives, httpResource passes the parsed JSON through parse before setting value(). If parse throws (e.g., ZodError), the resource transitions to 'error' status. The return type of parse determines the TypeScript type of value(), providing both runtime safety and compile-time types from a single schema definition.
Q: How does chain() handle errors from upstream resources?
When using chain(upstreamResource) in a resource's params function, the dependent resource does not execute its loader if the upstream resource is loading, idle, or in error state. If the upstream resource errors, the dependent resource transitions to error state with a ResourceDependencyError. This error includes the dependency (the upstream resource) and cause (the actual upstream error), allowing you to distinguish "my request failed" from "my dependency's request failed".
For a deeper dive into Angular signals and how they integrate with the broader framework, the signals guide covers computed signals, effects, and the reactivity model.
Migrating from HttpClient Subscriptions to httpResource
Existing Angular applications typically fetch data with HttpClient subscriptions inside ngOnInit or use AsyncPipe with Observables. Migrating to httpResource follows a predictable pattern:
// BEFORE: manual subscription in ngOnInit
@Component({ /* ... */ })
export class BeforeComponent implements OnInit, OnDestroy {
private http = inject(HttpClient);
private destroy$ = new Subject<void>();
users: User[] = [];
loading = false;
error: string | null = null;
ngOnInit() {
this.loading = true;
this.http.get<User[]>('/api/users')
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (data) => { this.users = data; this.loading = false; },
error: (err) => { this.error = err.message; this.loading = false; },
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
// AFTER: httpResource handles lifecycle automatically
@Component({ /* ... */ })
export class AfterComponent {
users = httpResource<User[]>(() => '/api/users');
// No ngOnInit, no Subject, no manual unsubscribe
// Template uses users.value(), users.isLoading(), users.error()
}The migration eliminates lifecycle management boilerplate. Subscription cleanup happens automatically when the component is destroyed. Loading and error states are built into the resource, no separate boolean flags needed.
For applications already using standalone components, the migration is straightforward: replace the HttpClient injection and subscription logic with a single httpResource declaration.
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Sources
- Angular Resource API documentation โ official guide on resource(), rxResource(), and httpResource()
- What's new in Angular 22.0 โ Ninja Squad coverage of the stable Resource API and chain() helper
- Angular HTTP interceptors guide โ how interceptors work with httpResource
Key Takeaways for Angular Resource API
httpResourcereplaces manualHttpClientsubscriptions with a single reactive declaration that handles loading, error, and cancellation automatically- Use
resource()for promise-based APIs,rxResource()for Observable pipelines with RxJS operators, andhttpResource()for standard HTTP calls with interceptor support - The
chain()helper (Angular 22+) enables dependent resource loading with automatic status propagation; if the upstream fails, the dependent gets aResourceDependencyError - Status values are string literals (
'idle','loading','resolved','error','reloading','local') - The
parseoption onhttpResourceintegrates Zod or Valibot for runtime schema validation that also drives TypeScript types - Calling
value()on a resource in error state throws; always guard withhasValue()or checkstatus()first - All resource variants cancel in-flight requests automatically when dependencies change, preventing race conditions without manual cancellation logic
Can you spot the bug in Angular?
One real snippet, one hidden bug, one attempt a day. No account needed to try.

Written by
Anthony Fillion-MailletFounder of SharpSkill
Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.
Updated on October 13, 2026
Tags
Share
Related articles

Angular Signals and Computed in 2026: Fine-Grained Reactivity and Interview Questions
Master Angular Signals, computed, effect, and linkedSignal in Angular 20+. Learn fine-grained reactivity patterns, performance optimizations, and prepare for technical interviews.

NgRx Signal Store vs Classic NgRx in 2026: Which Should You Choose?
A comprehensive comparison of NgRx Signal Store and Classic NgRx for Angular state management. Learn when to use each approach with practical code examples.

RxJS in Angular 2026: Operators, Subjects and Signals Interop
RxJS in Angular 2026: master the operators, Subjects, and Signals interop patterns Angular developers use in production, plus the interview questions that come up most.