Lesson 21 of 30
HttpClient gets bytes over the wire; resource() and httpResource() wrap that fetch in a signal so your template just reads .value() and .isLoading() instead of subscribing.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
HttpClient is Angular's built-in service for talking to a server — http.get(url), http.post(url, body), and so on, returning RxJS Observables. You can now configure it with provideHttpClient(withFetch()) so it runs on the native Fetch API under the hood instead of XMLHttpRequest — same HttpClient API, different transport.
resource() and httpResource() sit a layer above that. Instead of you calling .subscribe(), cleaning up on destroy, and juggling loading/error state by hand, a resource wraps an async operation — a fetch, or any promise-returning function — and hands you back signals: .value(), .isLoading(), .error(). When a signal the resource depends on changes, it refetches automatically. No subscription to leak, no takeUntilDestroyed.
import { Component, signal } from '@angular/core';
import { httpResource } from '@angular/common/http';
interface User {
name: string;
email: string;
}
@Component({
selector: 'app-user-profile',
template: `
<input type="number" [value]="userId()" (input)="userId.set(+$any($event.target).value)" />
@if (user.isLoading()) {
<p>Loading…</p>
} @else if (user.error()) {
<p>Couldn't load that user.</p>
} @else if (user.value(); as u) {
<h2>{{ u.name }}</h2>
<p>{{ u.email }}</p>
}
`,
})
export class UserProfileComponent {
userId = signal(1);
user = httpResource<User>(() => `/api/users/${this.userId()}`);
}
httpResource takes a reactive function — here, one that reads userId() and returns a URL. Because it reads a signal, the resource knows exactly when to refetch: type a new id, userId changes, the URL string changes, and httpResource fires a new request and updates .value(), all without you writing a single .subscribe(). If you change userId again before the first request lands, the in-flight one is cancelled — you never race two responses into the same .value().
Under the hood, httpResource is built on HttpClient itself, so anything you've registered — interceptors, auth headers, base URL rewriting — still applies. It's a more ergonomic signal-shaped front door onto the same client, not a replacement for it. For non-HTTP async work (an IndexedDB read, a WASM call), the lower-level resource() takes the same shape but with a loader function you write yourself instead of a URL.
httpResource('/api/users/1') fetches once and never again — you lose the whole point. The function form, () => \/api/users/$``, is what lets the resource track a dependency and know when to refetch..value() is populated immediately. While the request is in flight, .value() is undefined — templates need to branch on .isLoading() (or check for undefined) rather than assuming data is already there on first render.resource()/httpResource() replace HttpClient outright. They're a signal-shaped wrapper around it — interceptors, withFetch(), and everything else you configured on HttpClient still runs underneath.This is the signals fundamentals lesson's signal()/computed() model applied to async data instead of local state — a resource is really just a signal that happens to update itself by fetching. It's also Angular's answer to the same problem React's data-fetching libraries solve: turning "a promise somewhere" into a value your render logic can read synchronously without you managing subscription lifecycles yourself.