Intermediate

Angular: Making HTTP Requests

HttpClient configuration, CRUD operations, error handling, retries and HTTP interceptors.

Demo application: PostPulse — blog post manager
API used: https://jsonplaceholder.typicode.com


Table of Contents

  1. Module 1 — HTTP Configuration in Angular
  2. Module 2 — CRUD Operations with HttpClient
  3. Module 3 — Error Handling and Retries
  4. Module 4 — HTTP Interceptors for Authentication and Logging

Module 1 — HTTP Configuration in Angular

Why Use HttpClient for REST APIs?

HttpClient is Angular’s built-in module for interacting with REST APIs. It uses RxJS observables to handle responses reactively.

Comparison: HttpClient vs Native Fetch API

CriteriaFetch API (native)Angular HttpClient
SizeLightweight (browser-native)Larger, but more complete
FrameworkAgnosticAngular-specific
Error handlingManualBuilt-in and customizable
JSON parsingManualAutomatic
Interceptors supportNoYes
TypeScript typingNoYes (via generics)
Reactive integrationLimitedSeamless (async pipe, signals)

Project Setup Architecture

graph TD
    A[app.config.ts] -->|provideHttpClient| B[Global HttpClient]
    C[environment.ts] -->|apiUrl| D[Services / Components]
    B -->|inject| D
    D -->|HTTP requests| E[REST API]

1. Global Configuration — app.config.ts

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

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withFetch()  // Recommended for server-side rendering
    )
  ]
};

2. Environment Files

// environment.ts
export const environment = {
  production: false,
  apiUrl: 'https://jsonplaceholder.typicode.com'
};

// environment.prod.ts
export const environment = {
  production: true,
  apiUrl: 'https://api.example.com'
};

3. Data Model — post.model.ts

export interface Post {
  id: number;
  title: string;
  body: string;
  userId: number;
}

4. First GET Request in Component

import { Component, Signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Post } from './post.model';
import { environment } from '../environments/environment';
import { toSignal } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
})
export class App {
  posts!: Signal<Post[]>;

  constructor(private http: HttpClient) {
    this.posts = toSignal(
      this.http.get<Post[]>(environment.apiUrl + '/posts'),
      { initialValue: [] }
    );
  }
}

HTTP Headers and Parameters

HTTP Headers — HttpHeaders

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

const headers = new HttpHeaders({
  'Content-Type': 'application/json',
  'X-Request-ID': 'abc-123'
});

this.http.get<Post[]>(url, { headers });

Query Parameters — HttpParams

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

const params = new HttpParams()
  .set('userId', '1')
  .set('category', 'tech');

// URL becomes: /posts?userId=1&category=tech
this.http.get<Post[]>(url, { params });

Module 2 — CRUD Operations with HttpClient

HTTP Methods Overview

graph LR
    A[CRUD] --> B[GET - Read]
    A --> C[POST - Create]
    A --> D[PUT - Create/Update]
    A --> E[DELETE - Delete]
    B --> F[http.get]
    C --> G[http.post]
    D --> H[http.put]
    E --> I[http.delete]

Fetching Data with GET

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Post } from './post.model';
import { environment } from '../environments/environment';

@Injectable({ providedIn: 'root' })
export class PostService {
  private apiUrl = environment.apiUrl;

  constructor(private http: HttpClient) {}

  fetchPosts(): Observable<Post[]> {
    return this.http.get<Post[]>(this.apiUrl + '/posts');
  }
}

Best practice: Always place HTTP requests in services, never directly in components.

Consuming the Service in a Component

@Component({ ... })
export class App {
  posts!: Signal<Post[]>;

  constructor(private postService: PostService) {
    this.posts = toSignal(
      this.postService.fetchPosts(),
      { initialValue: [] }
    );
  }
}

Creating and Updating with POST and PUT

createPost(post: Post): Observable<Post> {
  return this.http.post<Post>(this.apiUrl + '/posts', post);
}

updatePost(post: Partial<Post>): Observable<Post> {
  return this.http.put<Post>(this.apiUrl + `/posts/${post.id}`, post);
}

Form Management in Component

import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';

@Component({ ... })
export class App {
  postForm!: FormGroup;

  constructor(private postService: PostService, private fb: FormBuilder) {
    this.postForm = this.fb.group({
      id: [0],
      title: ['', Validators.required],
      body: ['', Validators.required],
      userId: [1]
    });
  }

  onSubmit() {
    if (this.postForm.valid) {
      const post = this.postForm.value as Post;
      if (post.id === 0) {
        this.postService.createPost(post).subscribe(() => {
          this.posts = toSignal(this.postService.fetchPosts(), { initialValue: [] });
        });
      } else {
        this.postService.updatePost(post).subscribe(() => {
          this.posts = toSignal(this.postService.fetchPosts(), { initialValue: [] });
        });
      }
      this.postForm.reset({ id: 0, userId: 1 });
    }
  }
}

Deleting Resources with DELETE

deletePost(id: number): Observable<void> {
  return this.http.delete<void>(this.apiUrl + `/posts/${id}`);
}
deletePost(id: number) {
  if (confirm('Are you sure you want to delete this post?')) {
    this.postService.deletePost(id).subscribe(() => {
      this.posts = toSignal(this.postService.fetchPosts(), { initialValue: [] });
    });
  }
}

Module 3 — Error Handling and Retries

The HttpErrorResponse Class

Angular provides HttpErrorResponse to encapsulate any error during an HTTP request:

  • status — HTTP code returned by the server (e.g., 404, 500)
  • message — error message
  • error — error object (can be ErrorEvent for client-side errors)

Two Types of HTTP Errors

graph TD
    A[HTTP Error] --> B{error instanceof ErrorEvent?}
    B -->|Yes| C[Client-side error]
    B -->|No| D[Server-side error]
    C --> E[Network issue, CORS, etc.]
    D --> F[404, 500, 401, etc.]

Error Handling Method

import { HttpErrorResponse } from '@angular/common/http';
import { throwError } from 'rxjs';

private handleError(error: HttpErrorResponse) {
  let errorMessage = 'An unknown error occurred';

  if (error.error instanceof ErrorEvent) {
    errorMessage = `Client-side error: ${error.error.message}`;
  } else {
    errorMessage = `Server-side error: ${error.status} - ${error.message}`;
  }

  return throwError(() => new Error(errorMessage));
}

Complete Service with Error Handling

@Injectable({ providedIn: 'root' })
export class PostService {
  private apiUrl = environment.apiUrl;

  constructor(private http: HttpClient) {}

  fetchPosts(): Observable<Post[]> {
    return this.http.get<Post[]>(this.apiUrl + '/posts').pipe(
      retry({ count: 3, delay: (error, count) => timer(1000 * count) }),
      catchError(this.handleError)
    );
  }

  createPost(post: Post): Observable<Post> {
    return this.http.post<Post>(this.apiUrl + '/posts', post).pipe(
      catchError(this.handleError)
    );
  }

  updatePost(post: Partial<Post>): Observable<Post> {
    return this.http.put<Post>(this.apiUrl + `/posts/${post.id}`, post).pipe(
      catchError(this.handleError)
    );
  }

  deletePost(id: number): Observable<void> {
    return this.http.delete<void>(this.apiUrl + `/posts/${id}`).pipe(
      catchError(this.handleError)
    );
  }

  private handleError(error: HttpErrorResponse) {
    let errorMessage = 'An unknown error occurred';
    if (error.error instanceof ErrorEvent) {
      errorMessage = `Client-side error: ${error.error.message}`;
    } else {
      errorMessage = `Server-side error: ${error.status} - ${error.message}`;
    }
    return throwError(() => new Error(errorMessage));
  }
}

Retry Strategies

import { retry } from 'rxjs/operators';
import { timer } from 'rxjs';

// Fixed retry count
this.http.get<Post[]>(url).pipe(retry(3));

// Configurable retries with exponential backoff (recommended)
this.http.get<Post[]>(url).pipe(
  retry({
    count: 3,
    delay: (error, count) => timer(1000 * count)  // 1s, 2s, 3s
  }),
  catchError(this.handleError)
);

Displaying Errors to the User

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class App {
  error = signal<string | null>(null);

  constructor(private postService: PostService, private snackbar: MatSnackBar) {
    this.posts = toSignal(
      this.postService.fetchPosts().pipe(
        catchError((error: Error) => {
          this.error.set(error.message);
          return of([]);
        })
      ),
      { initialValue: [] }
    );
  }

  onSubmit() {
    if (this.postForm.valid) {
      this.postService.createPost(this.postForm.value).subscribe({
        next: () => { /* refresh */ },
        error: (err: Error) => {
          this.error.set(err.message);
          this.snackbar.open(err.message, 'Close', { duration: 3000 });
        }
      });
    }
  }
}
<!-- role="alert" important for ARIA accessibility -->
<div *ngIf="error()" role="alert"
     class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6">
  An error occurred: {{ error() }}
</div>

Module 4 — HTTP Interceptors for Authentication and Logging

What is an Interceptor?

Interceptors intercept HTTP requests and/or responses before they reach their destination.

Typical use cases:

  • Add common headers (authentication, correlation)
  • Log requests and responses
  • Transform response data
  • Implement caching
  • Centralize error handling

Interceptor Lifecycle

sequenceDiagram
    participant Client
    participant AuthInterceptor
    participant LoggingInterceptor
    participant API

    Client->>AuthInterceptor: HTTP Request
    AuthInterceptor->>LoggingInterceptor: Request + Authorization header
    LoggingInterceptor->>API: Logged request
    API-->>LoggingInterceptor: Response
    LoggingInterceptor-->>AuthInterceptor: Logged response
    AuthInterceptor-->>Client: Final response

Auth Interceptor — auth.interceptor.ts

import { HttpContextToken, HttpEventType, HttpInterceptorFn } from '@angular/common/http';
import { tap } from 'rxjs';

// Context token to skip the interceptor
export const SKIP_AUTH = new HttpContextToken(() => '');

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  if (req.context.get(SKIP_AUTH)) {
    return next(req);
  }

  const token = localStorage.getItem('token') || 'bearer-token';

  // Always clone the request before modifying
  const authReq = req.clone({
    setHeaders: {
      Authorization: `Bearer ${token}`
    }
  });

  return next(authReq).pipe(
    tap(event => {
      if (event.type === HttpEventType.Response) {
        console.log('Auth response received');
      }
    })
  );
};

Best practice: Always clone the request before modifying it (req.clone()). This ensures immutability.

Logging Interceptor — logging.interceptor.ts

import { HttpInterceptorFn, HttpEventType } from '@angular/common/http';
import { tap } from 'rxjs';

export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
  console.log('Outgoing request:', req.url, req.method);

  return next(req).pipe(
    tap((event) => {
      if (event.type === HttpEventType.Response) {
        console.log('Response received:', event.status, event.body);
      }
    })
  );
};

Registering Interceptors in app.config.ts

import { withInterceptors } from '@angular/common/http';
import { authInterceptor } from './auth.interceptor';
import { loggingInterceptor } from './logging.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withFetch(),
      withInterceptors([authInterceptor, loggingInterceptor])
      // Order matters: authInterceptor runs first
    ),
  ]
};

Skipping an Interceptor with HttpContext

import { HttpContext } from '@angular/common/http';
import { SKIP_AUTH } from './auth.interceptor';

// This request bypasses the auth interceptor
this.http.post('/public/login', credentials, {
  context: new HttpContext().set(SKIP_AUTH, true)
});

Complete Architecture Summary

graph TD
    subgraph Config ["app.config.ts"]
        PC[provideHttpClient]
        WF[withFetch]
        WI[withInterceptors]
        PC --> WF
        PC --> WI
    end

    subgraph Interceptors ["Interceptors Layer"]
        AI[authInterceptor\nAdds Authorization header]
        LI[loggingInterceptor\nLogs requests & responses]
        WI --> AI
        WI --> LI
    end

    subgraph Service ["PostService"]
        FP[fetchPosts\nGET + retry + catchError]
        CP[createPost\nPOST + SKIP_AUTH]
        UP[updatePost\nPUT + catchError]
        DP[deletePost\nDELETE + catchError]
    end

    subgraph Component ["AppComponent"]
        SIG[posts: Signal]
        ERR[error: Signal]
        FORM[postForm: FormGroup]
    end

    AI --> LI
    LI --> Service
    Service --> Component

Best Practices Summary

PracticeDescription
Dedicated servicePlace all HTTP requests in services, never in components
TypeScript genericsAlways type responses: http.get<Post[]>(...)
Environment filesStore API URLs in environment files
withFetch()Use withFetch() as best practice (SSR ready)
Centralized handleErrorSingle shared handleError method in service
Retry with backoffUse retry({ count, delay }) with increasing delay
Clone requestsIn interceptors, always use req.clone()
HttpContextTokenUse to conditionally control interceptors
OnPush + signalsWith ChangeDetectionStrategy.OnPush, prefer signals
role=“alert”Add this ARIA attribute to error messages for accessibility

Search Terms

angular · http · requests · frontend · development · interceptor · component · error · handling · httpclient · service · app.config.ts · architecture · configuration · data · errors · get · headers · interceptors · logging · parameters

Interested in this course?

Contact us to book it or get a custom training plan for your team.