Talking to a backend is central to most Angular apps, and interceptors centralise auth and error handling.
`HttpClient` (from @angular/common/http) is Angular's HTTP API. It is available for injection by default and uses fetch under the hood. You add features with provideHttpClient(...) in app.config.ts, for example interceptors.
private http = inject(HttpClient);
getUsers() { return this.http.get<User[]>('/api/users'); }Methods (get, post, put, patch, delete) return an RxJS Observable. Key facts:
- Observables are cold: no request is sent until something subscribes (
.subscribe(), theasyncpipe,toSignal,firstValueFrom). Each subscription sends a new request. - The response body is parsed as JSON by default; the type parameter (
get<User[]>) is a compile-time assertion, not validation. - Options include
params,headers,responseTypeandobserve: 'response'for status and headers. - Errors arrive as
HttpErrorResponsein the error callback orcatchError.
For most read-only data in components, prefer httpResource (next lesson), which wraps HttpClient in signals.
Interceptors are functions that see every request and response:
export const authInterceptor: HttpInterceptorFn = (req, next) =>
next(req.clone({ setHeaders: { Authorization: `Bearer ${inject(Auth).token()}` } }));Register them with provideHttpClient(withInterceptors([authInterceptor])). Requests are immutable, so use clone. Interceptors handle auth headers, logging, retries, error toasts and loading indicators.
Example
import { Component, Service, inject, signal } from '@angular/core';
import { HttpClient, HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
import { catchError, of } from 'rxjs';
export type Todo = { id: number; title: string };
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
console.log(req.method, req.url); // POST /api/todos
return next(req.clone({ setHeaders: { 'X-Client': 'web' } }));
};
// app.config.ts: provideHttpClient(withInterceptors([loggingInterceptor]))
@Service()
export class TodoApi {
private http = inject(HttpClient);
list() { return this.http.get<Todo[]>('/api/todos', { params: { limit: 10 } }); }
create(title: string) { return this.http.post<Todo>('/api/todos', { title }); }
}
@Component({ selector: 'app-todo-add', template: `<button (click)="add()">Add</button> {{ error() }}` })
export class TodoAdd {
private api = inject(TodoApi);
protected error = signal('');
add() {
this.api.create('New task')
.pipe(catchError((e: HttpErrorResponse) => { this.error.set(e.message); return of(null); }))
.subscribe((todo) => console.log('created', todo)); // request starts here
}
}Remember
inject(HttpClient).get<T>(url)returns a cold Observable; nothing is sent until subscribed- The type parameter is not runtime validation
- Functional interceptors (
HttpInterceptorFn) clone and modify requests for every call
Watch out for
- Calling
this.http.post(...)without subscribing, so the request is never sent.