--- name: angular-http description: Implement HTTP data fetching in Angular v20+ using resource(), httpResource(), and HttpClient. Use for API calls, data loading with signals, request/response handling, and interceptors. Triggers on data fetching, API integration, loading states, error handling, or converting Observable-based HTTP to signal-based patterns. --- # Angular HTTP & Data Fetching Fetch data in Angular using signal-based `resource()`, `httpResource()`, and the traditional `HttpClient`. ## httpResource() - Signal-Based HTTP `httpResource()` wraps HttpClient with signal-based state management: ```typescript import { Component, signal } from "@angular/core"; import { httpResource } from "@angular/common/http"; interface User { id: number; name: string; email: string; } @Component({ selector: "app-user-profile", template: ` @if (userResource.isLoading()) {

Loading...

} @else if (userResource.error()) {

Error: {{ userResource.error()?.message }}

} @else if (userResource.hasValue()) {

{{ userResource.value().name }}

{{ userResource.value().email }}

} `, }) export class UserProfile { userId = signal("123"); // Reactive HTTP resource - refetches when userId changes userResource = httpResource(() => `/api/users/${this.userId()}`); } ``` ### httpResource Options ```typescript // Simple GET request userResource = httpResource(() => `/api/users/${this.userId()}`); // With full request options userResource = httpResource(() => ({ url: `/api/users/${this.userId()}`, method: "GET", headers: { Authorization: `Bearer ${this.token()}` }, params: { include: "profile" }, })); // With default value usersResource = httpResource(() => "/api/users", { defaultValue: [], }); // Skip request when params undefined userResource = httpResource(() => { const id = this.userId(); return id ? `/api/users/${id}` : undefined; }); ``` ### Resource State ```typescript // Status signals userResource.value(); // Current value or undefined userResource.hasValue(); // Boolean - has resolved value userResource.error(); // Error or undefined userResource.isLoading(); // Boolean - currently loading userResource.status(); // 'idle' | 'loading' | 'reloading' | 'resolved' | 'error' | 'local' // Actions userResource.reload(); // Manually trigger reload userResource.set(value); // Set local value userResource.update(fn); // Update local value ``` ## resource() - Generic Async Data For non-HTTP async operations or custom fetch logic: ```typescript import { resource, signal } from '@angular/core'; @Component({...}) export class Search { query = signal(''); searchResource = resource({ // Reactive params - triggers reload when changed params: () => ({ q: this.query() }), // Async loader function loader: async ({ params, abortSignal }) => { if (!params.q) return []; const response = await fetch(`/api/search?q=${params.q}`, { signal: abortSignal, }); return response.json() as Promise; }, }); } ``` ### Resource with Default Value ```typescript todosResource = resource({ defaultValue: [] as Todo[], params: () => ({ filter: this.filter() }), loader: async ({ params }) => { const res = await fetch(`/api/todos?filter=${params.filter}`); return res.json(); }, }); // value() returns Todo[] (never undefined) ``` ### Conditional Loading ```typescript const userId = signal(null); userResource = resource({ params: () => { const id = userId(); // Return undefined to skip loading return id ? { id } : undefined; }, loader: async ({ params }) => { return fetch(`/api/users/${params.id}`).then((r) => r.json()); }, }); // Status is 'idle' when params returns undefined ``` ## HttpClient - Traditional Approach For complex scenarios or when you need Observable operators: ```typescript import { Component, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { toSignal } from '@angular/core/rxjs-interop'; @Component({...}) export class Users { private http = inject(HttpClient); // Convert Observable to Signal users = toSignal( this.http.get('/api/users'), { initialValue: [] } ); // Or use Observable directly users$ = this.http.get('/api/users'); } ``` ### HTTP Methods ```typescript private http = inject(HttpClient); // GET getUser(id: string) { return this.http.get(`/api/users/${id}`); } // POST createUser(user: CreateUserDto) { return this.http.post('/api/users', user); } // PUT updateUser(id: string, user: UpdateUserDto) { return this.http.put(`/api/users/${id}`, user); } // PATCH patchUser(id: string, changes: Partial) { return this.http.patch(`/api/users/${id}`, changes); } // DELETE deleteUser(id: string) { return this.http.delete(`/api/users/${id}`); } ``` ### Request Options ```typescript this.http.get("/api/users", { headers: { Authorization: "Bearer token", "Content-Type": "application/json", }, params: { page: "1", limit: "10", sort: "name", }, observe: "response", // Get full HttpResponse responseType: "json", }); ``` ## Interceptors ### Functional Interceptor (Recommended) ```typescript // auth.interceptor.ts import { HttpInterceptorFn } from "@angular/common/http"; import { inject } from "@angular/core"; export const authInterceptor: HttpInterceptorFn = (req, next) => { const authService = inject(Auth); const token = authService.token(); if (token) { req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` }, }); } return next(req); }; // error.interceptor.ts export const errorInterceptor: HttpInterceptorFn = (req, next) => { return next(req).pipe( catchError((error: HttpErrorResponse) => { if (error.status === 401) { inject(Router).navigate(["/login"]); } return throwError(() => error); }), ); }; // logging.interceptor.ts export const loggingInterceptor: HttpInterceptorFn = (req, next) => { const started = Date.now(); return next(req).pipe( tap({ next: () => console.log(`${req.method} ${req.url} - ${Date.now() - started}ms`), error: (err) => console.error(`${req.method} ${req.url} failed`, err), }), ); }; ``` ### Register Interceptors ```typescript // app.config.ts import { provideHttpClient, withInterceptors } from "@angular/common/http"; export const appConfig: ApplicationConfig = { providers: [ provideHttpClient( withInterceptors([authInterceptor, errorInterceptor, loggingInterceptor]), ), ], }; ``` ## Error Handling ### With httpResource ```typescript @Component({ template: ` @if (userResource.error(); as error) {

{{ getErrorMessage(error) }}

} `, }) export class UserCmpt { userResource = httpResource(() => `/api/users/${this.userId()}`); getErrorMessage(error: unknown): string { if (error instanceof HttpErrorResponse) { return ( error.error?.message || `Error ${error.status}: ${error.statusText}` ); } return "An unexpected error occurred"; } } ``` ### With HttpClient ```typescript import { catchError, retry } from 'rxjs'; getUser(id: string) { return this.http.get(`/api/users/${id}`).pipe( retry(2), // Retry up to 2 times catchError((error: HttpErrorResponse) => { console.error('Error fetching user:', error); return throwError(() => new Error('Failed to load user')); }) ); } ``` ## Loading States Pattern ```typescript @Component({ template: ` @switch (dataResource.status()) { @case ("idle") {

Enter a search term

} @case ("loading") { } @case ("reloading") { } @case ("resolved") { } @case ("error") { } } `, }) export class Data { query = signal(""); dataResource = httpResource(() => this.query() ? `/api/search?q=${this.query()}` : undefined, ); } ``` For advanced patterns, see [references/http-patterns.md](references/http-patterns.md).