HttpClient and interceptors

Angular · Practical · 14 min

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(), the async pipe, 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, responseType and observe: 'response' for status and headers.
  • Errors arrive as HttpErrorResponse in the error callback or catchError.

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.

Check yourself

  1. A service method returns this.http.delete('/api/items/1'), and a click handler calls it without subscribe(). What happens?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: angular.dev/guide/http