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.
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 exposesvalue(),isLoading(),error()andstatus(). Returnundefinedto skip the request.HttpClient(observables) remains the tool for mutations (POST/PUT/DELETE) and complex flows.- Functional interceptors (
HttpInterceptorFn) registered withprovideHttpClient(withInterceptors([...]))add headers, logging, retries and error handling for every request. provideHttpClientTesting()+HttpTestingControllerintercept requests in tests.
The code
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, theasyncpipe, ortoSignal. - Using
httpResourcefor POST/PUT/DELETE; it's for reading data. UseHttpClientfor 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.