On this page

HttpClient: consuming REST APIs

15 min read TextCh. 4 — Integration

HttpClient in Angular

HttpClient is Angular's built-in service for communicating with REST APIs. It returns typed Observables and supports interceptors, error handling, and transformations.

Configuring HttpClient

In app.config.ts, provide the HTTP client:

import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withFetch(),           // Use native Fetch API
      withInterceptors([authInterceptor]),
    ),
  ],
};

HTTP methods

HttpClient provides methods for each HTTP verb:

const http = inject(HttpClient);

// GET - Retrieve data
http.get<User[]>('/api/users');

// POST - Create resource
http.post<User>('/api/users', { name: 'Ana' });

// PUT - Replace entire resource
http.put<User>('/api/users/1', completeData);

// PATCH - Partially update
http.patch<User>('/api/users/1', { name: 'Ana Maria' });

// DELETE - Remove resource
http.delete<void>('/api/users/1');

Query parameters

Use HttpParams to add query parameters:

import { HttpParams } from '@angular/common/http';

const params = new HttpParams()
  .set('page', 1)
  .set('limit', 20)
  .set('sort', 'date');

http.get<Result>('/api/search', { params });
// GET /api/search?page=1&limit=20&sort=date

Custom headers

import { HttpHeaders } from '@angular/common/http';

const headers = new HttpHeaders()
  .set('Authorization', `Bearer ${token}`)
  .set('Accept-Language', 'en');

http.get('/api/data', { headers });

Functional interceptors

Interceptors process ALL HTTP requests and responses:

import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const auth = inject(AuthService);
  const token = auth.token();

  if (token) {
    const authReq = req.clone({
      setHeaders: { Authorization: `Bearer ${token}` },
    });
    return next(authReq);
  }

  return next(req);
};

Register interceptors in the configuration:

provideHttpClient(
  withInterceptors([authInterceptor, loggingInterceptor]),
)

Error handling

Use catchError from RxJS to handle HTTP errors:

import { catchError } from 'rxjs/operators';
import { of, throwError } from 'rxjs';

getUser(id: number) {
  return this.http.get<User>(`/api/users/${id}`).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 404) {
        return of(null);  // Return null if not found
      }
      return throwError(() => error);  // Re-throw other errors
    }),
  );
}

HttpClient with resource

You can combine HttpClient with rxResource for reactive loading:

import { rxResource } from '@angular/core/rxjs-interop';

readonly userId = signal(1);

readonly user = rxResource({
  request: () => ({ id: this.userId() }),
  loader: ({ request }) =>
    this.http.get<User>(`/api/users/${request.id}`),
});

Best practices

  1. Type the responses — Always use generics: get<MyType>()
  2. Centralize in services — Don't call HttpClient from components
  3. Handle errors — Use catchError for failed responses
  4. Use interceptors — For authentication, logging, and retry

Practice

  1. Build a CRUD service: Implement a service with getAll(), getById(), create(), and remove() methods using HttpClient against https://jsonplaceholder.typicode.com/posts. Type all responses with generics.
  2. Add a logging interceptor: Create a functional interceptor that logs the URL and method of every HTTP request to the console. Register it with withInterceptors() in the configuration.
  3. Handle errors with catchError: In the getById() method, use catchError to return null when the server responds with 404 and re-throw other errors.

In the next lesson, we will learn the essential RxJS operators for transforming and combining data streams.

provideHttpClient
Register HttpClient in app.config.ts with provideHttpClient(withFetch()). The withFetch() option uses the browser's native Fetch API, which is more efficient than XMLHttpRequest.
Subscriptions
HttpClient returns cold Observables that automatically complete after the response. But in components, remember to clean up long-lived subscriptions with takeUntilDestroyed() or DestroyRef.
import { Injectable, inject, signal, computed } from '@angular/core';
import { HttpClient, HttpParams, HttpErrorResponse } from '@angular/common/http';
import { catchError, tap, map } from 'rxjs/operators';
import { of } from 'rxjs';

interface Article {
  id: number;
  title: string;
  content: string;
  author: string;
  date: string;
}

interface PageResponse {
  data: Article[];
  total: number;
  page: number;
}

@Injectable({ providedIn: 'root' })
export class ArticlesService {
  private readonly http = inject(HttpClient);
  private readonly baseUrl = '/api/articles';

  private readonly _loading = signal(false);
  readonly loading = this._loading.asReadonly();

  getAll(page = 1, limit = 10) {
    this._loading.set(true);

    const params = new HttpParams()
      .set('page', page)
      .set('limit', limit);

    return this.http
      .get<PageResponse>(this.baseUrl, { params })
      .pipe(
        tap(() => this._loading.set(false)),
        catchError((error: HttpErrorResponse) => {
          this._loading.set(false);
          console.error('Error loading articles:', error.message);
          return of({ data: [], total: 0, page: 1 });
        }),
      );
  }

  getById(id: number) {
    return this.http.get<Article>(`${this.baseUrl}/${id}`);
  }

  create(article: Omit<Article, 'id'>) {
    return this.http.post<Article>(this.baseUrl, article);
  }

  update(id: number, changes: Partial<Article>) {
    return this.http.patch<Article>(
      `${this.baseUrl}/${id}`,
      changes,
    );
  }

  remove(id: number) {
    return this.http.delete<void>(`${this.baseUrl}/${id}`);
  }
}