How to Handle Multiple API Calls Simultaneously in Angular
In enterprise applications, we often need to handle multiple API calls efficiently. Let me share my approach using a real-world example from an e-commerce dashboard I developed.
The dashboard needed to:
- Load user profile
- Fetch recent orders
- Get product recommendations
- Load cart items
- Check user permissions
Here’s how I implemented it:
// 1. API Service with Advanced Error Handling
@Injectable({ providedIn: 'root' })
export class DashboardService {
private http = inject(HttpClient);
private errorHandler = inject(ErrorHandlingService);
// Fetch all required data
loadDashboardData(userId: string): Observable<DashboardData> {
// Define all API calls
const profile$ = this.getUserProfile(userId).pipe(
catchError(error => {
this.errorHandler.handleError(error);
return of(null);
})
);
const orders$ = this.getRecentOrders(userId).pipe(
catchError(error => {
this.errorHandler.handleError(error);
return of([]);
})
);
const recommendations$ = this.getRecommendations(userId).pipe(
catchError(error => {
this.errorHandler.handleError(error);
return of([]);
})
);
const cart$ = this.getCartItems(userId).pipe(
catchError(error => {
this.errorHandler.handleError(error);
return of([]);
})
);
const permissions$ = this.getUserPermissions(userId).pipe(
catchError(error => {
this.errorHandler.handleError(error);
return of({});
})
);
// Combine all calls
return forkJoin({
profile: profile$,
orders: orders$,
recommendations: recommendations$,
cart: cart$,
permissions: permissions$
}).pipe(
timeout(30000), // 30-second timeout
retry(2), // Retry failed requests
catchError(error => {
if (error instanceof TimeoutError) {
this.errorHandler.handleTimeout();
}
throw error;
})
);
}
// Individual API calls with caching
private getUserProfile(userId: string) {
return this.http.get<UserProfile>(
`/api/users/${userId}`
).pipe(
shareReplay(1) // Cache the response
);
}
private getRecentOrders(userId: string) {
return this.http.get<Order[]>(
`/api/users/${userId}/orders`
).pipe(
map(orders => orders.slice(0, 5))
);
}
// ... other API methods
}
// 2. Dashboard Component Implementation
@Component({
selector: 'app-dashboard',
template: `
@if (loading()) {
<loading-spinner />
}
@if (error()) {
<error-alert [message]="error()" />
}
@if (dashboardData(); as data) {
<!-- User Profile -->
@if (data.profile) {
<user-profile [data]="data.profile" />
}
<!-- Recent Orders -->
@if (data.orders.length) {
<recent-orders [orders]="data.orders" />
}
<!-- Recommendations -->
@if (data.recommendations.length) {
<product-recommendations
[products]="data.recommendations"
/>
}
<!-- Cart Items -->
@if (data.cart.length) {
<shopping-cart [items]="data.cart" />
}
}
`
})
export class DashboardComponent {
private service = inject(DashboardService);
private destroy$ = inject(DestroyRef);
// State management with signals
loading = signal(true);
error = signal<string | null>(null);
dashboardData = signal<DashboardData | null>(null);
ngOnInit() {
// Get user ID from auth service
const userId = this.auth.getCurrentUserId();
// Load all dashboard data
this.service.loadDashboardData(userId).pipe(
takeUntilDestroyed(this.destroy$)
).subscribe({
next: (data) => {
this.loading.set(false);
this.dashboardData.set(data);
},
error: (err) => {
this.loading.set(false);
this.error.set(
'Failed to load dashboard data. Please try again.'
);
}
});
}
}
// 3. Progressive Loading Strategy
@Injectable({ providedIn: 'root' })
export class ProgressiveLoadingService {
loadCriticalData(userId: string): Observable<CriticalData> {
// Load critical data first (profile & permissions)
return forkJoin({
profile: this.getUserProfile(userId),
permissions: this.getUserPermissions(userId)
});
}
loadNonCriticalData(userId: string): Observable<NonCriticalData> {
// Load non-critical data after
return forkJoin({
orders: this.getRecentOrders(userId),
recommendations: this.getRecommendations(userId)
});
}
}
// 4. Parallel vs Sequential Loading
@Component({
template: `
@if (criticalData(); as data) {
<critical-content [data]="data" />
@defer (on viewport) {
<non-critical-content />
}
}
`
})
export class OptimizedDashboardComponent {
private loading = inject(ProgressiveLoadingService);
criticalData = signal<CriticalData | null>(null);
ngOnInit() {
const userId = this.auth.getCurrentUserId();
// Load critical data immediately
this.loading.loadCriticalData(userId).pipe(
takeUntilDestroyed(this.destroy$)
).subscribe(data => {
this.criticalData.set(data);
// Load non-critical data when critical content is visible
this.loading.loadNonCriticalData(userId).pipe(
takeUntilDestroyed(this.destroy$)
).subscribe(nonCritical => {
// Update UI progressively
});
});
}
}
Real-world optimization example:
// Before optimization
ngOnInit() {
const userId = this.auth.getCurrentUserId();
this.http.get(`/api/profile/${userId}`).subscribe();
this.http.get(`/api/orders/${userId}`).subscribe();
this.http.get(`/api/recommendations/${userId}`).subscribe();
}
// After optimization
ngOnInit() {
const userId = this.auth.getCurrentUserId();
// 1. Combine related calls
const userProfile$ = this.http.get(`/api/profile/${userId}`).pipe(
shareReplay(1) // Cache the result
);
const userOrders$ = this.http.get(`/api/orders/${userId}`).pipe(
shareReplay(1)
);
// 2. Use combineLatest for dependent data
combineLatest({
profile: userProfile$,
orders: userOrders$
}).pipe(
switchMap(({ profile, orders }) => {
// Use profile and orders to get personalized recommendations
return this.http.get(
`/api/recommendations/${userId}`,
{ params: {
categories: orders.map(o => o.category),
preferences: profile.preferences
}}
);
}),
retry(3),
takeUntilDestroyed(this.destroy$)
).subscribe(recommendations => {
// Handle all data
});
}
Key points I emphasize in interviews:
Error Handling
- Handle individual API failures
- Implement retry logic
- Provide fallback data
Performance
- Use appropriate RxJS operators
- Implement caching
- Progressive loading
User Experience
- Show loading states
- Handle errors gracefully
- Load critical data first
Best Practices
- Proper error handling
- Resource cleanup
- Caching strategies
- Type safety
This approach has helped me build robust enterprise applications that handle multiple API calls efficiently while maintaining good user experience.