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:

  1. Load user profile
  2. Fetch recent orders
  3. Get product recommendations
  4. Load cart items
  5. 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:

  1. Error Handling

    • Handle individual API failures
    • Implement retry logic
    • Provide fallback data
  2. Performance

    • Use appropriate RxJS operators
    • Implement caching
    • Progressive loading
  3. User Experience

    • Show loading states
    • Handle errors gracefully
    • Load critical data first
  4. 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.

Test Your Knowledge

Take a quick quiz to test your understanding of this topic.