HTTP Client and API Communication¶
Course Objective¶
Master Angular's HTTP client for consuming REST APIs, handling responses, implementing error handling, and managing HTTP requests effectively in real-world applications.
1. Introduction to Angular HTTP Client¶
Angular's HTTP client is built on top of the Fetch API and provides a powerful, type-safe way to communicate with backend services. It returns Observables, making it perfect for handling asynchronous operations.
Key Features¶
- Type-safe HTTP requests
- Request and response interceptors
- Automatic JSON parsing
- Error handling
- Request cancellation
- Progress monitoring
2. Setting Up HTTP Client¶
Modern Setup (Standalone Components)¶
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(withInterceptorsFromDi()),
// other providers
]
});
Traditional Module Setup¶
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
3. Making HTTP Requests¶
Basic HTTP Service¶
// services/api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface User {
id: number;
name: string;
email: string;
created_at: string;
}
export interface CreateUserRequest {
name: string;
email: string;
}
@Injectable({
providedIn: 'root'
})
export class ApiService {
private readonly baseUrl = 'https://api.example.com';
constructor(private http: HttpClient) {}
// GET request
getUsers(): Observable<User[]> {
return this.http.get<User[]>(`${this.baseUrl}/users`);
}
// GET with parameters
getUserById(id: number): Observable<User> {
return this.http.get<User>(`${this.baseUrl}/users/${id}`);
}
// GET with query parameters
searchUsers(query: string, page: number = 1): Observable<User[]> {
const params = new HttpParams()
.set('q', query)
.set('page', page.toString());
return this.http.get<User[]>(`${this.baseUrl}/users/search`, { params });
}
// POST request
createUser(userData: CreateUserRequest): Observable<User> {
const headers = new HttpHeaders({
'Content-Type': 'application/json'
});
return this.http.post<User>(`${this.baseUrl}/users`, userData, { headers });
}
// PUT request
updateUser(id: number, userData: Partial<CreateUserRequest>): Observable<User> {
return this.http.put<User>(`${this.baseUrl}/users/${id}`, userData);
}
// DELETE request
deleteUser(id: number): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/users/${id}`);
}
// Custom headers
getProtectedData(): Observable<any> {
const headers = new HttpHeaders({
'Authorization': 'Bearer ' + this.getAuthToken(),
'Content-Type': 'application/json'
});
return this.http.get<any>(`${this.baseUrl}/protected`, { headers });
}
private getAuthToken(): string {
return localStorage.getItem('authToken') || '';
}
}
Using the Service in Components¶
// components/user-list.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ApiService, User } from '../services/api.service';
import { Subject } from 'rxjs';
import { takeUntil, finalize } from 'rxjs/operators';
@Component({
selector: 'app-user-list',
template: `
<div class="user-list">
<h2>Users</h2>
<div *ngIf="loading" class="loading">Loading users...</div>
<div *ngIf="error" class="error">
Error: {{ error }}
<button (click)="retry()">Retry</button>
</div>
<div *ngIf="users.length > 0" class="users">
<div *ngFor="let user of users" class="user-card">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<button (click)="deleteUser(user.id)">Delete</button>
</div>
</div>
<button (click)="loadUsers()" [disabled]="loading">
Refresh
</button>
</div>
`,
styles: [`
.user-list { padding: 20px; }
.user-card {
border: 1px solid #ddd;
padding: 15px;
margin: 10px 0;
border-radius: 4px;
}
.loading, .error {
padding: 15px;
border-radius: 4px;
}
.loading { background-color: #e3f2fd; }
.error { background-color: #ffebee; color: #c62828; }
`]
})
export class UserListComponent implements OnInit, OnDestroy {
users: User[] = [];
loading = false;
error: string | null = null;
private destroy$ = new Subject<void>();
constructor(private apiService: ApiService) {}
ngOnInit(): void {
this.loadUsers();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
loadUsers(): void {
this.loading = true;
this.error = null;
this.apiService.getUsers()
.pipe(
takeUntil(this.destroy$),
finalize(() => this.loading = false)
)
.subscribe({
next: (users) => {
this.users = users;
},
error: (error) => {
this.error = 'Failed to load users';
console.error('Error loading users:', error);
}
});
}
deleteUser(id: number): void {
if (confirm('Are you sure you want to delete this user?')) {
this.apiService.deleteUser(id)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: () => {
this.users = this.users.filter(user => user.id !== id);
},
error: (error) => {
this.error = 'Failed to delete user';
console.error('Error deleting user:', error);
}
});
}
}
retry(): void {
this.loadUsers();
}
}
4. Handling HTTP Responses¶
Response Types¶
// services/advanced-api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpResponse, HttpEventType } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, filter } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class AdvancedApiService {
constructor(private http: HttpClient) {}
// Get full response (including headers)
getUserWithHeaders(id: number): Observable<HttpResponse<User>> {
return this.http.get<User>(`/api/users/${id}`, {
observe: 'response'
});
}
// Get response body only (default)
getUser(id: number): Observable<User> {
return this.http.get<User>(`/api/users/${id}`);
}
// Get events (for progress monitoring)
uploadFile(file: File): Observable<any> {
const formData = new FormData();
formData.append('file', file);
return this.http.post('/api/upload', formData, {
observe: 'events',
reportProgress: true
}).pipe(
map(event => {
switch (event.type) {
case HttpEventType.UploadProgress:
const progress = Math.round(100 * event.loaded / event.total!);
return { type: 'progress', progress };
case HttpEventType.Response:
return { type: 'complete', body: event.body };
default:
return { type: 'other', event };
}
})
);
}
// Working with response headers
getDataWithEtag(): Observable<{ data: any; etag: string }> {
return this.http.get<any>('/api/data', { observe: 'response' })
.pipe(
map(response => ({
data: response.body,
etag: response.headers.get('ETag') || ''
}))
);
}
}
5. Error Handling¶
Comprehensive Error Handling¶
// services/error-handling.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError, of, timer } from 'rxjs';
import { catchError, retry, retryWhen, switchMap, take } from 'rxjs/operators';
export interface ApiError {
message: string;
status: number;
timestamp: Date;
}
@Injectable({
providedIn: 'root'
})
export class ErrorHandlingService {
constructor(private http: HttpClient) {}
// Basic error handling
getDataWithErrorHandling(): Observable<any> {
return this.http.get('/api/data')
.pipe(
catchError(this.handleError)
);
}
// Retry on failure
getDataWithRetry(): Observable<any> {
return this.http.get('/api/data')
.pipe(
retry(3), // Retry up to 3 times
catchError(this.handleError)
);
}
// Advanced retry with delay
getDataWithAdvancedRetry(): Observable<any> {
return this.http.get('/api/data')
.pipe(
retryWhen(errors =>
errors.pipe(
switchMap((error, index) => {
// Retry only for certain errors and limited times
if (index >= 3 || error.status === 404) {
return throwError(error);
}
// Exponential backoff: 1s, 2s, 4s
return timer(Math.pow(2, index) * 1000);
})
)
),
catchError(this.handleError)
);
}
// Fallback data on error
getDataWithFallback(): Observable<any> {
return this.http.get('/api/data')
.pipe(
catchError(error => {
console.warn('API failed, using fallback data:', error);
return of({ message: 'Fallback data', items: [] });
})
);
}
private handleError = (error: HttpErrorResponse): Observable<never> => {
let errorMessage = 'An unknown error occurred';
if (error.error instanceof ErrorEvent) {
// Client-side error
errorMessage = `Client Error: ${error.error.message}`;
} else {
// Server-side error
switch (error.status) {
case 400:
errorMessage = 'Bad Request: ' + this.extractErrorMessage(error);
break;
case 401:
errorMessage = 'Unauthorized: Please log in again';
this.handleUnauthorized();
break;
case 403:
errorMessage = 'Forbidden: You do not have permission';
break;
case 404:
errorMessage = 'Not Found: The requested resource was not found';
break;
case 500:
errorMessage = 'Internal Server Error: Please try again later';
break;
case 503:
errorMessage = 'Service Unavailable: Server is temporarily unavailable';
break;
default:
errorMessage = `Server Error: ${error.status} - ${error.message}`;
}
}
// Log error to console (in production, send to logging service)
console.error('HTTP Error:', {
message: errorMessage,
status: error.status,
url: error.url,
error: error.error
});
return throwError({
message: errorMessage,
status: error.status,
timestamp: new Date()
} as ApiError);
}
private extractErrorMessage(error: HttpErrorResponse): string {
if (error.error?.message) {
return error.error.message;
}
if (error.error?.error) {
return error.error.error;
}
return error.message;
}
private handleUnauthorized(): void {
// Clear auth token and redirect to login
localStorage.removeItem('authToken');
// In a real app, you might use Router to navigate
// this.router.navigate(['/login']);
}
}
6. HTTP Interceptors¶
Authentication Interceptor¶
// interceptors/auth.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Get auth token
const authToken = localStorage.getItem('authToken');
// Clone request and add authorization header
if (authToken) {
const authReq = req.clone({
setHeaders: {
Authorization: `Bearer ${authToken}`
}
});
return next.handle(authReq);
}
return next.handle(req);
}
}
Loading Interceptor¶
// interceptors/loading.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { LoadingService } from '../services/loading.service';
@Injectable()
export class LoadingInterceptor implements HttpInterceptor {
constructor(private loadingService: LoadingService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Show loading indicator
this.loadingService.show();
return next.handle(req).pipe(
finalize(() => {
// Hide loading indicator when request completes
this.loadingService.hide();
})
);
}
}
Error Interceptor¶
// interceptors/error.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { NotificationService } from '../services/notification.service';
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(private notificationService: NotificationService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
catchError((error: HttpErrorResponse) => {
// Show user-friendly error messages
if (error.status >= 400 && error.status < 500) {
this.notificationService.showError('Client error occurred');
} else if (error.status >= 500) {
this.notificationService.showError('Server error occurred');
}
return throwError(error);
})
);
}
}
Registering Interceptors¶
// Modern setup (main.ts)
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AuthInterceptor, LoadingInterceptor, ErrorInterceptor } from './interceptors';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(
withInterceptors([AuthInterceptor, LoadingInterceptor, ErrorInterceptor])
)
]
});
// Traditional setup (app.module.ts)
import { HTTP_INTERCEPTORS } from '@angular/common/http';
@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: LoadingInterceptor,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: ErrorInterceptor,
multi: true
}
]
})
export class AppModule {}
7. Request and Response Transformation¶
Custom HTTP Service with Transformation¶
// services/transformed-api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
export interface ApiUser {
id: number;
first_name: string;
last_name: string;
email_address: string;
created_timestamp: string;
}
export interface User {
id: number;
fullName: string;
email: string;
createdAt: Date;
}
@Injectable({
providedIn: 'root'
})
export class TransformedApiService {
constructor(private http: HttpClient) {}
// Transform API response to match client model
getUsers(): Observable<User[]> {
return this.http.get<ApiUser[]>('/api/users')
.pipe(
map(apiUsers => apiUsers.map(this.transformApiUser)),
tap(users => console.log('Transformed users:', users))
);
}
// Transform single user
getUser(id: number): Observable<User> {
return this.http.get<ApiUser>(`/api/users/${id}`)
.pipe(
map(this.transformApiUser)
);
}
// Transform client model to API format
createUser(user: Omit<User, 'id' | 'createdAt'>): Observable<User> {
const apiUser = this.transformToApiUser(user);
return this.http.post<ApiUser>('/api/users', apiUser)
.pipe(
map(this.transformApiUser)
);
}
private transformApiUser = (apiUser: ApiUser): User => ({
id: apiUser.id,
fullName: `${apiUser.first_name} ${apiUser.last_name}`,
email: apiUser.email_address,
createdAt: new Date(apiUser.created_timestamp)
});
private transformToApiUser(user: Partial<User>): Partial<ApiUser> {
const [firstName, ...lastNameParts] = (user.fullName || '').split(' ');
return {
first_name: firstName || '',
last_name: lastNameParts.join(' ') || '',
email_address: user.email || ''
};
}
}
8. Caching Strategies¶
HTTP Cache Service¶
// services/cache.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of, timer } from 'rxjs';
import { tap, shareReplay, switchMap } from 'rxjs/operators';
interface CacheEntry<T> {
data: T;
timestamp: number;
expiry: number;
}
@Injectable({
providedIn: 'root'
})
export class CacheService {
private cache = new Map<string, CacheEntry<any>>();
private readonly DEFAULT_CACHE_TIME = 5 * 60 * 1000; // 5 minutes
constructor(private http: HttpClient) {}
// Get data with caching
get<T>(url: string, cacheTime?: number): Observable<T> {
const cacheKey = url;
const cached = this.cache.get(cacheKey);
const now = Date.now();
// Return cached data if valid
if (cached && now < cached.expiry) {
return of(cached.data);
}
// Fetch fresh data
return this.http.get<T>(url).pipe(
tap(data => {
this.cache.set(cacheKey, {
data,
timestamp: now,
expiry: now + (cacheTime || this.DEFAULT_CACHE_TIME)
});
}),
shareReplay(1)
);
}
// Clear specific cache entry
clearCache(url: string): void {
this.cache.delete(url);
}
// Clear all cache
clearAllCache(): void {
this.cache.clear();
}
// Auto-refresh cache
getWithAutoRefresh<T>(url: string, refreshInterval: number): Observable<T> {
return timer(0, refreshInterval).pipe(
switchMap(() => {
this.clearCache(url);
return this.get<T>(url);
})
);
}
}
9. Testing HTTP Services¶
Testing HTTP Service¶
// services/api.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ApiService, User } from './api.service';
describe('ApiService', () => {
let service: ApiService;
let httpTestingController: HttpTestingController;
const baseUrl = 'https://api.example.com';
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [ApiService]
});
service = TestBed.inject(ApiService);
httpTestingController = TestBed.inject(HttpTestingController);
});
afterEach(() => {
// Verify that no unmatched requests are outstanding
httpTestingController.verify();
});
describe('getUsers', () => {
it('should fetch users', () => {
const mockUsers: User[] = [
{ id: 1, name: 'John Doe', email: 'john@example.com', created_at: '2023-01-01' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', created_at: '2023-01-02' }
];
service.getUsers().subscribe(users => {
expect(users).toEqual(mockUsers);
});
const req = httpTestingController.expectOne(`${baseUrl}/users`);
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
});
describe('createUser', () => {
it('should create a user', () => {
const newUser = { name: 'New User', email: 'new@example.com' };
const createdUser: User = {
id: 3,
...newUser,
created_at: '2023-01-03'
};
service.createUser(newUser).subscribe(user => {
expect(user).toEqual(createdUser);
});
const req = httpTestingController.expectOne(`${baseUrl}/users`);
expect(req.request.method).toBe('POST');
expect(req.request.body).toEqual(newUser);
expect(req.request.headers.get('Content-Type')).toBe('application/json');
req.flush(createdUser);
});
});
describe('error handling', () => {
it('should handle HTTP errors', () => {
const errorMessage = 'Server error';
service.getUsers().subscribe({
next: () => fail('Expected error'),
error: (error) => {
expect(error.status).toBe(500);
}
});
const req = httpTestingController.expectOne(`${baseUrl}/users`);
req.flush(errorMessage, { status: 500, statusText: 'Internal Server Error' });
});
});
});
Testing Interceptors¶
// interceptors/auth.interceptor.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
import { AuthInterceptor } from './auth.interceptor';
describe('AuthInterceptor', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
]
});
httpClient = TestBed.inject(HttpClient);
httpTestingController = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpTestingController.verify();
});
it('should add authorization header when token exists', () => {
const token = 'test-token';
spyOn(localStorage, 'getItem').and.returnValue(token);
httpClient.get('/api/data').subscribe();
const req = httpTestingController.expectOne('/api/data');
expect(req.request.headers.get('Authorization')).toBe(`Bearer ${token}`);
req.flush({});
});
it('should not add authorization header when token does not exist', () => {
spyOn(localStorage, 'getItem').and.returnValue(null);
httpClient.get('/api/data').subscribe();
const req = httpTestingController.expectOne('/api/data');
expect(req.request.headers.has('Authorization')).toBeFalsy();
req.flush({});
});
});
10. Best Practices¶
1. Service Organization¶
// Good: Organized API service with clear structure
@Injectable({
providedIn: 'root'
})
export class UserApiService {
private readonly baseUrl = environment.apiUrl + '/users';
constructor(private http: HttpClient) {}
// Group related methods
getAll(): Observable<User[]> { /* ... */ }
getById(id: number): Observable<User> { /* ... */ }
create(user: CreateUserRequest): Observable<User> { /* ... */ }
update(id: number, user: UpdateUserRequest): Observable<User> { /* ... */ }
delete(id: number): Observable<void> { /* ... */ }
}
2. Type Safety¶
// Always use TypeScript interfaces for requests and responses
export interface ApiResponse<T> {
data: T;
message: string;
success: boolean;
}
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
pageSize: number;
}
// Use generic methods for reusability
class BaseApiService {
protected get<T>(url: string): Observable<T> {
return this.http.get<T>(url);
}
protected post<T>(url: string, body: any): Observable<T> {
return this.http.post<T>(url, body);
}
}
3. Error Handling Strategy¶
// Consistent error handling across services
abstract class BaseService {
protected handleError<T>(operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
console.error(`${operation} failed:`, error);
// Let the app keep running by returning an empty result
return of(result as T);
};
}
}
4. Performance Optimization¶
// Use shareReplay to avoid multiple requests
@Injectable({
providedIn: 'root'
})
export class ConfigService {
private config$ = this.http.get<Config>('/api/config').pipe(
shareReplay(1) // Cache the result and share with all subscribers
);
getConfig(): Observable<Config> {
return this.config$;
}
}
5. Request Cancellation¶
// Component with request cancellation
export class SearchComponent implements OnDestroy {
private destroy$ = new Subject<void>();
private searchTerms = new Subject<string>();
constructor(private apiService: ApiService) {
this.searchTerms.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term =>
term ? this.apiService.searchUsers(term) : of([])
),
takeUntil(this.destroy$)
).subscribe(results => {
this.searchResults = results;
});
}
search(term: string): void {
this.searchTerms.next(term);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
Common Pitfalls to Avoid¶
- Not unsubscribing from HTTP requests - Use takeUntil pattern
- Making multiple identical requests - Use shareReplay
- Not handling errors properly - Always implement error handling
- Ignoring loading states - Show loading indicators
- Not using TypeScript types - Always type your HTTP responses
- Blocking the UI with synchronous operations - Use async patterns
- Not testing HTTP services - Use HttpClientTestingModule
Next Steps¶
After mastering HTTP client and API communication, you'll be ready to dive into:
- Observables and RxJS: Learn advanced reactive programming patterns
- State Management: Manage application state effectively
- Forms: Build complex forms with validation
- Routing: Implement navigation and route guards
The HTTP client is fundamental to most Angular applications, so practice these patterns until they become second nature!