Angular 20 in 2026: Resource API, httpResource and Interview Questions

Angular 20 introduces httpResource and stabilizes the Resource API for signal-based data fetching. A hands-on tutorial covering resource(), rxResource(), httpResource(), Zod validation, and common interview questions.

Angular 20 Resource API and httpResource tutorial for reactive data fetching with signals

Angular 20 brings the Resource API and httpResource to the forefront of signal-based data fetching. These experimental APIs replace verbose HttpClient subscription patterns with reactive primitives that track loading, error, and resolved states automatically.

What changed in Angular 20

The Resource API renames request to params and loader to stream (for rxResource). Status values are now string literals ('idle', 'loading', 'resolved', 'error', 'reloading', 'local') instead of numeric enums. httpResource builds on HttpClient, supporting interceptors and Zod validation out of the box.

The Three Resource Variants in Angular 20

Angular 20 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 using fetch() or any promise-based API.
  • rxResource() works with Observables. The right choice when RxJS operators like debounceTime, retry, or switchMap are needed.
  • httpResource() wraps Angular's HttpClient directly. 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 major pain point 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.

user-profile.component.tstypescript
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.

product-list.component.tstypescript
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
  }
}

Several details matter here. 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 for reads only

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.

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.

order.component.tstypescript
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 in Angular 20

Angular 20 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.

search.component.tstypescript
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($any($event.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

Angular 20 changes ResourceStatus from a numeric enum to 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() |

status-demo.component.tstypescript
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.

Accessing value() in error state

Since Angular 20, 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 breaking change from Angular 19 where value() returned undefined on error.

Angular 20 httpResource Interview Questions

The Resource API is becoming 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.

For a deeper dive into Angular signals and how they integrate with the broader framework, the signals module 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:

typescript
// 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.

Conclusion

  • httpResource replaces manual HttpClient subscriptions 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, and httpResource() for standard HTTP calls with interceptor support
  • Angular 20 renames request to params and loader to stream in rxResource — update existing code accordingly
  • Status values are now string literals ('idle', 'loading', 'resolved', 'error', 'reloading', 'local'), replacing numeric enums from Angular 19
  • The parse option on httpResource integrates Zod or Valibot for runtime schema validation that also drives TypeScript types
  • Calling value() on a resource in error state throws in Angular 20 — always guard with hasValue() or check status() first
  • All resource variants cancel in-flight requests automatically when dependencies change, preventing race conditions without manual cancellation logic

Tags

#angular
#angular-20
#resource-api
#httpresource
#signals
#typescript
#tutorial

Share

Related articles