Demo application: PostPulse — blog post manager
API used:https://jsonplaceholder.typicode.com
Table of Contents
- Module 1 — HTTP Configuration in Angular
- Module 2 — CRUD Operations with HttpClient
- Module 3 — Error Handling and Retries
- 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
| Criteria | Fetch API (native) | Angular HttpClient |
|---|---|---|
| Size | Lightweight (browser-native) | Larger, but more complete |
| Framework | Agnostic | Angular-specific |
| Error handling | Manual | Built-in and customizable |
| JSON parsing | Manual | Automatic |
| Interceptors support | No | Yes |
| TypeScript typing | No | Yes (via generics) |
| Reactive integration | Limited | Seamless (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
Recommended Pattern: Dedicated Service — post.service.ts
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 messageerror— error object (can beErrorEventfor 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
| Practice | Description |
|---|---|
| Dedicated service | Place all HTTP requests in services, never in components |
| TypeScript generics | Always type responses: http.get<Post[]>(...) |
| Environment files | Store API URLs in environment files |
| withFetch() | Use withFetch() as best practice (SSR ready) |
| Centralized handleError | Single shared handleError method in service |
| Retry with backoff | Use retry({ count, delay }) with increasing delay |
| Clone requests | In interceptors, always use req.clone() |
| HttpContextToken | Use to conditionally control interceptors |
| OnPush + signals | With 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