Modern Angular: Signals, Standalone Components and SSR · Components, Signals and Data

HTTP with httpResource, HttpClient and Interceptors

Load data reactively with httpResource, call APIs with HttpClient, add cross-cutting behaviour with functional interceptors, and test with HttpTestingController.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A product search that loads results with httpResource: the request URL depends on a query signal, so changing the query triggers a new request automatically, and the component shows loading, results and errors. An interceptor adds an auth header and logs requests. Requests are answered by HttpTestingController, so the example runs without a server.

The idea

  • httpResource(() => url or request) makes a reactive GET: it re-requests when signals used in the function change and exposes value(), isLoading(), error() and status(). Return undefined to skip the request.
  • HttpClient (observables) remains the tool for mutations (POST/PUT/DELETE) and complex flows.
  • Functional interceptors (HttpInterceptorFn) registered with provideHttpClient(withInterceptors([...])) add headers, logging, retries and error handling for every request.
  • provideHttpClientTesting() + HttpTestingController intercept requests in tests.

The code

httpResource driven by a signal, with an interceptor
import { Component, signal } from "@angular/core";
import { HttpInterceptorFn, httpResource, provideHttpClient, withInterceptors } from "@angular/common/http";
import { HttpTestingController, provideHttpClientTesting } from "@angular/common/http/testing";
import { TestBed } from "@angular/core/testing";
import { BrowserTestingModule, platformBrowserTesting } from "@angular/platform-browser/testing";

const authInterceptor: HttpInterceptorFn = (req, next) => {
  console.log(`  -> ${req.method} ${req.urlWithParams}`);
  return next(req.clone({ setHeaders: { Authorization: "Bearer demo-token" } }));
};

type Product = { id: number; name: string };

@Component({
  selector: "app-search",
  template: `
    @if (results.isLoading()) { <p>Loading...</p> }
    @else if (results.error()) { <p>Search failed, try again.</p> }
    @else {
      <ul>@for (p of results.value(); track p.id) { <li>{{ p.name }}</li> } @empty { <li>No results</li> }</ul>
    }
  `,
})
class Search {
  query = signal("");
  results = httpResource<Product[]>(() => {
    const q = this.query().trim();
    return q.length < 2 ? undefined : { url: "/api/products", params: { q } };   // undefined: no request
  }, { defaultValue: [] });
}

async function main() {
  TestBed.initTestEnvironment(BrowserTestingModule, platformBrowserTesting());
  TestBed.configureTestingModule({
    providers: [provideHttpClient(withInterceptors([authInterceptor])), provideHttpClientTesting()],
  });
  const fixture = TestBed.createComponent(Search);
  const http = TestBed.inject(HttpTestingController);
  const text = () => [...fixture.nativeElement.querySelectorAll("li, p")].map((n: Element) => n.textContent!.trim()).join(", ");

  fixture.componentInstance.query.set("t");
  await fixture.whenStable();
  console.log(`query "t": ${text()} (no request for short queries)`);

  fixture.componentInstance.query.set("tea");
  fixture.detectChanges();
  console.log(`query "tea": ${text()}`);
  const req = http.expectOne((r) => r.url === "/api/products");
  console.log(`  header Authorization: ${req.request.headers.get("Authorization")}`);
  req.flush([{ id: 1, name: "Teapot" }, { id: 2, name: "Tea strainer" }]);
  await fixture.whenStable();
  console.log(`results: ${text()}`);

  fixture.componentInstance.query.set("kettle");
  fixture.detectChanges();
  http.expectOne((r) => r.url === "/api/products").flush("down", { status: 503, statusText: "Unavailable" });
  await fixture.whenStable();
  console.log(`query "kettle": ${text()}`);
  http.verify();
}
main();

Output

query "t": No results (no request for short queries)
  -> GET /api/products?q=tea
query "tea": Loading...
  header Authorization: Bearer demo-token
results: Teapot, Tea strainer
  -> GET /api/products?q=kettle
query "kettle": Search failed, try again.

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Subscribing manually in components and forgetting to unsubscribe; prefer httpResource, the async pipe, or toSignal.
  • Using httpResource for POST/PUT/DELETE; it's for reading data. Use HttpClient for mutations.
  • Adding auth headers in every service call instead of one interceptor (and sending tokens to third-party URLs; check the request URL in the interceptor).
  • Ignoring the error state, leaving users with a spinner forever.

Key points

  • httpResource gives reactive, signal-based GET requests with loading and error state.
  • HttpClient for mutations; functional interceptors for cross-cutting concerns.
  • HttpTestingController answers requests in tests.

Further reading: httpResource · Interceptors · Testing HTTP requests

Practice questions

When does httpResource send a new request?

Answer

Whenever a signal read inside its request function changes; returning undefined skips the request.

What are functional interceptors used for?

Answer

Cross-cutting behaviour for every request, such as adding auth headers, logging, retries or error handling, registered with withInterceptors.

Why is httpResource not used for POST requests?

Answer

It's designed for reactive data reading: it re-runs when dependencies change and can cancel in-flight requests, which is wrong for mutations; use HttpClient for those.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?