Skip to main content

Secure API Communication Design Patterns and Implementation Strategies

Comprehensive guide to designing secure API communication systems with authentication, encryption, and protection against common security threats.

Secure API communication forms the backbone of modern web applications, requiring robust authentication mechanisms, end-to-end encryption, and comprehensive protection against common attack vectors like injection, man-in-the-middle, and session hijacking threats.

1. Problem Statement & Requirements

Clarifying questions

  1. What target browsers/devices are we supporting? Are we targeting mobile-first, desktop-only, or cross-platform with specific browser version requirements (e.g., IE11 support)?
  2. What authentication/authorization model is in place? Will we be using JWT tokens, OAuth2, or session-based auth? Do we need to support multi-tenant or role-based access control?
  3. Is this API communication for real-time data (WebSocket) or primarily static HTTP requests? Will we need to handle long polling, server-sent events, or real-time updates?
  4. What are the offline support requirements? Should the app gracefully handle network failures and queue requests when offline, or is it strictly online-first?
  5. What accessibility standards must we meet? Are we targeting WCAG 2.1 AA compliance or higher, and what are the specific assistive technology requirements?
  6. Do we need internationalization support? Will this API communication layer handle i18n, and if so, what are the translation key patterns and locale switching requirements?
  7. What design system or component library are we using? Are there existing patterns for API communication that we must integrate with, or do we have full freedom to design?

Functional requirements (user-facing)

  • Secure transmission of API requests over HTTPS
  • Automatic token refresh and authentication handling
  • Request/response interception for logging and transformation
  • Error handling with user-friendly feedback
  • Support for various HTTP methods (GET, POST, PUT, DELETE)
  • API rate limiting and retry logic
  • Client-side request caching for performance optimization
  • Support for API versioning and endpoint routing

Non-functional requirements

  • Performance: All API calls must complete within 3 seconds for 95% of requests
  • Security: All API communication must use TLS 1.3+ and follow OWASP security guidelines
  • Scalability: API client must support concurrent requests up to 50 simultaneously
  • Reliability: Retry logic for transient network errors with exponential backoff (max 3 retries)
  • Accessibility: All API communication feedback must be accessible to screen readers and keyboard navigation
  • Maintainability: Clear separation between API client logic and business logic

Constraints and assumptions

  • All API endpoints are assumed to be HTTPS-only (no HTTP fallback)
  • Authentication tokens will be managed via a dedicated auth service or session storage
  • We assume the backend API follows REST conventions with JSON payloads
  • The frontend will not handle sensitive data in local storage (tokens are the exception)
  • We assume a modern browser environment with ES2020+ support
  • Authentication headers will be added automatically to all outgoing requests

Out of scope

  • Direct database access from frontend (all data must go through API layer)
  • Client-side caching of sensitive user data
  • Implementation of backend authentication services (JWT token generation, etc.)
  • Support for legacy browsers (IE11 and below)
  • Integration with third-party authentication providers beyond basic token handling
  • API documentation generation or OpenAPI spec integration
  • Mobile-specific optimizations (push notifications, background sync)

2. Component Architecture

graph TD
    A[APICommunicationManager] --> B[AuthInterceptor]
    A --> C[RequestQueue]
    A --> D[RetryHandler]
    A --> E[ErrorBoundary]
    A --> F[SecurityValidator]
    B --> G[TokenManager]
    C --> H[RateLimiter]
    D --> I[ExponentialBackoff]
    F --> J[SignatureValidator]
    F --> K[ContentSecurityPolicy]
    
    subgraph CoreComponents
        A
        B
        C
        D
        E
        F
    end
    
    subgraph SupportingComponents
        G
        H
        I
        J
        K
    end

Component Tree Breakdown

Smart Components (Container Components):

  • APICommunicationManager - Main orchestrator that manages all API communication flows
  • AuthInterceptor - Handles authentication token injection and refresh logic
  • RequestQueue - Manages concurrent request limits and queuing strategy
  • RetryHandler - Implements retry logic with exponential backoff
  • SecurityValidator - Validates request/response security properties

Dumb Components (Presentational Components):

  • TokenManager - Token storage and retrieval utilities
  • RateLimiter - Request rate limiting implementation
  • SignatureValidator - Request signature validation logic
  • ContentSecurityPolicy - CSP header management

TypeScript Props Contracts

// APICommunicationManager props
interface APICommunicationManagerProps {
  children: React.ReactNode;
  baseUrl: string;
  defaultHeaders?: Record<string, string>;
  maxRetries?: number;
  retryDelayBase?: number;
  maxConcurrentRequests?: number;
  timeoutMs?: number;
}

// AuthInterceptor props
interface AuthInterceptorProps {
  onTokenRefresh: (oldToken?: string) => Promise<string>;
  tokenStorageKey: string;
  onAuthFailure: () => void;
}

// SecurityValidator props
interface SecurityValidatorProps {
  validateSignature?: (request: APIRequest) => boolean;
  validateCSP?: (response: APIResponse) => boolean;
  allowedContentTypes?: string[];
}

UI State Machine

stateDiagram-v2
    [*] --> Idle
    Idle --> Requesting : sendRequest()
    Requesting --> Success : response.success
    Requesting --> Error : response.error
    Error --> Retry : retryEnabled && !maxRetriesExceeded
    Retry --> Requesting : retry()
    Error --> AuthFailure : authError
    AuthFailure --> RefreshToken : onAuthFailure()
    RefreshToken --> Requesting : tokenRefreshed
    Success --> Idle : cleanup()
    
    [*] --> WaitingForResponse : response.pending
    WaitingForResponse --> Success : response.success
    WaitingForResponse --> Error : response.error || timeout

Component Responsibilities and Props

APICommunicationManager:

  • Main orchestrator for all API communication
  • Manages global configuration and middleware chain
  • Handles request queuing, rate limiting, and retry logic
  • Propagates security validation errors to error boundary

AuthInterceptor:

  • Injects authentication tokens into outgoing requests
  • Handles token refresh when expired
  • Triggers auth failure handler on 401 responses

RequestQueue:

  • Enforces concurrent request limits
  • Queues requests when limits exceeded
  • Implements priority-based queuing strategy

RetryHandler:

  • Implements exponential backoff retry logic
  • Handles different error types with appropriate retry strategies
  • Manages max retry attempts and exponential delay calculation

SecurityValidator:

  • Validates request/response security properties
  • Implements signature verification for authenticated requests
  • Enforces content security policies on responses

Reusability Decisions

The component architecture follows a composition pattern where:

  • Core components are reusable across different API communication scenarios
  • Security validation is modular and can be selectively enabled/disabled
  • Authentication logic is abstracted to support different auth providers
  • Rate limiting and retry strategies are configurable per API endpoint

Component Lifecycle Considerations

All components implement proper cleanup in useEffect and useCallback to:

  • Cancel pending requests on component unmount
  • Clear retry timers and queued requests
  • Remove event listeners for token refresh
  • Clean up authentication state when session ends

The component lifecycle is designed to handle:

  • Session expiration during long-running requests
  • Concurrent request cancellation when navigating away
  • Token refresh during active API calls
  • Graceful degradation when security validation fails
// Example usage of the component architecture
const App = () => {
  return (
    <APICommunicationManager
      baseUrl="https://api.example.com"
      maxRetries={3}
      maxConcurrentRequests={5}
    >
      <AuthInterceptor
        onTokenRefresh={refreshAuthToken}
        tokenStorageKey="auth_token"
        onAuthFailure={() => navigate('/login')}
      >
        <SecurityValidator
          validateSignature={validateRequestSignature}
          validateCSP={validateContentSecurityPolicy}
        >
          <AppRoutes />
        </SecurityValidator>
      </AuthInterceptor>
    </APICommunicationManager>
  );
};

3. State Management Design

Local state vs. global state decisions

For secure API communication, we need to carefully distinguish between local and global state:

  • Local state: Component-specific UI state (loading indicators, form fields, error messages)
  • Global state: Authentication tokens, API configuration, request queues, security headers
  • Server state: API responses, cached data, optimistic updates

Authentication tokens and security headers should be global to ensure consistent security across all API calls. Request queues and retry logic benefit from centralized management.

State management approach

I'll use React Query as the primary state management solution with Context API for security context:

Justification:

  • React Query handles caching, invalidation, and server state management natively
  • Context API provides clean authentication token propagation without prop drilling
  • React Query's built-in retry and error handling reduces boilerplate
  • Better developer experience than Redux Toolkit for API-heavy applications

State shape and normalization strategy

// Global security context state
interface SecurityState {
  accessToken: string | null;
  refreshToken: string | null;
  isAuthenticated: boolean;
  tokenExpiry: Date | null;
}

// API request queue state
interface RequestQueueState {
  pendingRequests: ApiRequest[];
  activeRequests: Set<string>;
}

// API response state
interface ApiResponse<T> {
  data: T | null;
  error: ApiError | null;
  isLoading: boolean;
  isFetched: boolean;
}

// Normalized API state structure
interface ApiState {
  security: SecurityState;
  queue: RequestQueueState;
  cache: Map<string, ApiResponse<any>>;
}

Server state vs. client state separation

Server state includes:

  • API responses (cached, stale, fresh)
  • Authentication tokens
  • User permissions and roles

Client state includes:

  • UI loading indicators
  • Form validation states
  • Modal visibility
  • Error messages

Cache invalidation strategy

  1. Explicit invalidation: Clear cache on user actions (logout, data changes)
  2. Time-based expiration: 5-minute TTL for API responses
  3. Network status invalidation: Clear cache on network errors
  4. Mutation-based invalidation: Invalidate related queries after write operations

Optimistic updates design

For secure API communication, optimistic updates must be carefully implemented:

  • Only apply to write operations (POST/PUT/PATCH)
  • Validate server response matches optimistic changes
  • Rollback on server errors with proper error handling
  • Include security context in optimistic updates

Critical-path hook implementation

// apiClient.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { createContext, useContext, useEffect, useState } from 'react';

interface ApiRequest {
  id: string;
  url: string;
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
  headers: Record<string, string>;
  body?: any;
  retries?: number;
}

interface ApiError {
  status: number;
  message: string;
  code?: string;
}

// Security context
interface AuthContextType {
  accessToken: string | null;
  refreshToken: string | null;
  isAuthenticated: boolean;
  login: (credentials: { username: string; password: string }) => Promise<void>;
  logout: () => void;
  refreshAuth: () => Promise<boolean>;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

// Main API client hook
export function useSecureApiClient() {
  const queryClient = useQueryClient();
  const [auth, setAuth] = useState<SecurityState>({
    accessToken: null,
    refreshToken: null,
    isAuthenticated: false,
    tokenExpiry: null
  });

  // Authentication hook
  const loginMutation = useMutation({
    mutationFn: async (credentials: { username: string; password: string }) => {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials)
      });
      
      if (!response.ok) {
        throw new Error('Authentication failed');
      }
      
      return response.json();
    },
    onSuccess: (data) => {
      setAuth({
        accessToken: data.accessToken,
        refreshToken: data.refreshToken,
        isAuthenticated: true,
        tokenExpiry: new Date(Date.now() + data.expiresIn * 1000)
      });
      
      // Store tokens in secure storage (e.g., HttpOnly cookies or secure local storage)
      localStorage.setItem('accessToken', data.accessToken);
      localStorage.setItem('refreshToken', data.refreshToken);
    }
  });

  // Secure API call with automatic token refresh
  const secureApiCall = async <T>(
    request: Omit<ApiRequest, 'headers'>,
    options?: {
      retryOnAuthError?: boolean;
      cacheKey?: string;
    }
  ): Promise<T> => {
    const baseHeaders = {
      'Content-Type': 'application/json',
      'Accept': 'application/json'
    };

    const headers = {
      ...baseHeaders,
      'Authorization': `Bearer ${auth.accessToken}`
    };

    try {
      const response = await fetch(request.url, {
        method: request.method,
        headers,
        body: request.body ? JSON.stringify(request.body) : undefined
      });

      if (response.status === 401 && options?.retryOnAuthError) {
        // Attempt to refresh token
        const refreshed = await refreshAuth();
        if (refreshed) {
          // Retry the original request with new token
          return secureApiCall<T>({ ...request, headers }, options);
        }
      }

      if (!response.ok) {
        throw new Error(`API Error: ${response.status}`);
      }

      return response.json();
    } catch (error) {
      if (error instanceof Error) {
        throw new Error(`Secure API Call Failed: ${error.message}`);
      }
      throw new Error('Unknown error occurred');
    }
  };

  // Mutation with optimistic updates
  const useSecureMutation = <T, Variables = any>(
    mutationKey: string,
    mutationFn: (variables: Variables) => Promise<T>,
    options?: {
      optimisticUpdate?: (variables: Variables, currentData: T | null) => T;
      invalidateQueries?: string[];
    }
  ) => {
    return useMutation<T, Error, Variables>({
      mutationFn: async (variables) => {
        const result = await mutationFn(variables);
        
        // Invalidate related queries
        if (options?.invalidateQueries) {
          queryClient.invalidateQueries({
            queryKey: options.invalidateQueries
          });
        }
        
        return result;
      },
      onMutate: async (variables) => {
        if (options?.optimisticUpdate) {
          // Cancel ongoing queries for this key
          await queryClient.cancelQueries({ queryKey: [mutationKey] });
          
          // Snapshot the previous value
          const previousData = queryClient.getQueryData<T>([mutationKey]);
          
          // Optimistically update the data
          queryClient.setQueryData<T>([mutationKey], (old) => 
            options.optimisticUpdate?.(variables, old) || old
          );
          
          return { previousData };
        }
      },
      onError: async (error, variables, context) => {
        // Rollback to previous data
        if (context?.previousData) {
          queryClient.setQueryData([mutationKey], context.previousData);
        }
      },
      onSuccess: async (data, variables) => {
        // Refetch queries after successful mutation
        await queryClient.invalidateQueries({ queryKey: [mutationKey] });
      }
    });
  };

  // Token refresh logic
  const refreshAuth = async (): Promise<boolean> => {
    if (!auth.refreshToken) return false;
    
    try {
      const response = await fetch('/api/auth/refresh', {
        method: 'POST',
        headers: { 
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${auth.refreshToken}`
        }
      });

      if (!response.ok) {
        throw new Error('Token refresh failed');
      }

      const data = await response.json();
      setAuth({
        accessToken: data.accessToken,
        refreshToken: data.refreshToken,
        isAuthenticated: true,
        tokenExpiry: new Date(Date.now() + data.expiresIn * 1000)
      });

      localStorage.setItem('accessToken', data.accessToken);
      localStorage.setItem('refreshToken', data.refreshToken);
      
      return true;
    } catch (error) {
      setAuth({
        accessToken: null,
        refreshToken: null,
        isAuthenticated: false,
        tokenExpiry: null
      });
      localStorage.removeItem('accessToken');
      localStorage.removeItem('refreshToken');
      return false;
    }
  };

  // Cleanup on logout
  const logout = () => {
    setAuth({
      accessToken: null,
      refreshToken: null,
      isAuthenticated: false,
      tokenExpiry: null
    });
    localStorage.removeItem('accessToken');
    localStorage.removeItem('refreshToken');
  };

  // Auto-refresh token before expiry
  useEffect(() => {
    if (auth.tokenExpiry && auth.isAuthenticated) {
      const timeUntilExpiry = auth.tokenExpiry.getTime() - Date.now();
      const refreshTimeout = Math.max(0, timeUntilExpiry - 60000); // Refresh 1 minute before expiry
      
      const timeout = setTimeout(() => {
        refreshAuth();
      }, refreshTimeout);

      return () => clearTimeout(timeout);
    }
  }, [auth.tokenExpiry, auth.isAuthenticated]);

  return {
    auth,
    login: loginMutation.mutateAsync,
    logout,
    refreshAuth,
    secureApiCall,
    useSecureMutation
  };
}

// Usage example in component
export function SecureComponent() {
  const { auth, login, logout, secureApiCall } = useSecureApiClient();
  
  const handleLogin = async () => {
    try {
      await login({ username: 'user', password: 'pass' });
    } catch (error) {
      console.error('Login failed:', error);
    }
  };

  const fetchSecureData = async () => {
    try {
      const data = await secureApiCall<{ items: string[] }>({
        url: '/api/secure-endpoint',
        method: 'GET'
      });
      console.log(data);
    } catch (error) {
      console.error('API call failed:', error);
    }
  };

  return (
    <div>
      {auth.isAuthenticated ? (
        <button onClick={logout}>Logout</button>
      ) : (
        <button onClick={handleLogin}>Login</button>
      )}
    </div>
  );
}

The hook provides:

  • Authentication management with automatic token refresh
  • Secure API calls with proper error handling and retry logic
  • Optimistic updates for write operations with rollback capability
  • Cache invalidation and query management through React Query
  • Security context propagation throughout the application
  • Type safety with TypeScript interfaces for all API interactions

This design ensures that security is handled consistently across all API communications while maintaining good performance and developer experience.

4. Routing & Navigation

Route Structure and Hierarchy

graph TD
    A[Root] --> B[Public Routes]
    A --> C[Protected Routes]
    A --> D[Error Routes]
    
    B --> E[Login]
    B --> F[Register]
    B --> G[Forgot Password]
    
    C --> H[Dashboard]
    C --> I[Profile]
    C --> J[Settings]
    C --> K[Admin]
    
    D --> L[404]
    D --> M[500]

The application follows a protected route pattern with clear separation between public and authenticated routes. The root path serves as the application shell, while protected routes require authentication via JWT tokens.

Code Splitting Strategy

Route-based code splitting with dynamic imports:

// routes/index.ts
import { lazy } from 'react';

export const routes = [
  {
    path: '/login',
    component: lazy(() => import('pages/Login')),
    isPublic: true,
  },
  {
    path: '/dashboard',
    component: lazy(() => import('pages/Dashboard')),
    isProtected: true,
  },
  {
    path: '/admin',
    component: lazy(() => import('pages/Admin')),
    isProtected: true,
    requiresAdmin: true,
  },
];

Justification:

  • Public routes (login, register) are loaded immediately as they're needed for initial authentication flow
  • Protected routes are split to reduce initial bundle size and improve load times
  • Admin route is separated as it contains heavy dependencies only needed by administrators
// guards/AuthGuard.tsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from 'stores/auth';

export const AuthGuard = ({ children }: { children: React.ReactNode }) => {
  const navigate = useNavigate();
  const { isAuthenticated, user, isLoading } = useAuthStore();

  useEffect(() => {
    if (isLoading) return;
    
    if (!isAuthenticated) {
      navigate('/login', { replace: true });
    } else if (user?.role === 'admin' && window.location.pathname === '/dashboard') {
      navigate('/admin', { replace: true });
    }
  }, [isAuthenticated, user, isLoading, navigate]);

  return children;
};

Deep Linking Support and URL Parameter Design

// types/routing.ts
export interface RouteParams {
  userId?: string;
  projectId?: string;
  taskId?: string;
}

export interface QueryParams {
  tab?: 'overview' | 'activity' | 'settings';
  filter?: string;
  page?: number;
}

// URL structure examples:
// /dashboard?tab=overview&filter=active&page=1
// /projects/123/tasks/456?tab=activity

Browser History Management and Scroll Restoration

// hooks/useScrollRestoration.ts
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

export const useScrollRestoration = () => {
  const location = useLocation();

  useEffect(() => {
    // Restore scroll position when navigating back
    const handlePopState = () => {
      window.scrollTo(0, 0);
    };

    window.addEventListener('popstate', handlePopState);
    
    return () => {
      window.removeEventListener('popstate', handlePopState);
    };
  }, [location]);

  // Reset scroll to top on route change
  useEffect(() => {
    window.scrollTo(0, 0);
  }, [location]);
};

Route Configuration with Nested Layouts

// layouts/MainLayout.tsx
import { Outlet } from 'react-router-dom';
import Header from 'components/Header';
import Sidebar from 'components/Sidebar';

export const MainLayout = () => {
  return (
    <div className="flex flex-col min-h-screen">
      <Header />
      <div className="flex flex-1">
        <Sidebar />
        <main className="flex-1 p-6">
          <Outlet />
        </main>
      </div>
    </div>
  );
};

// routes/AppRoutes.tsx
import { createBrowserRouter } from 'react-router-dom';
import { MainLayout } from 'layouts/MainLayout';

const router = createBrowserRouter([
  {
    path: '/',
    element: <MainLayout />,
    children: [
      {
        index: true,
        element: <Dashboard />,
      },
      {
        path: 'profile',
        element: <Profile />,
      },
      {
        path: 'admin',
        element: <Admin />,
        handle: { requiresAdmin: true },
      },
    ],
  },
  {
    path: '/auth',
    element: <AuthLayout />,
    children: [
      {
        path: 'login',
        element: <Login />,
      },
      {
        path: 'register',
        element: <Register />,
      },
    ],
  },
]);

Authentication Flow Sequence

sequenceDiagram
    participant U as User
    participant A as AuthGuard
    participant S as AuthStore
    participant R as Router
    
    U->>A: Navigate to protected route
    A->>S: Check authentication status
    S-->>A: isAuthenticated = false
    A->>R: Redirect to /login
    U->>R: Navigate to /login
    R->>S: Authenticate user
    S-->>R: Store auth token
    R->>A: Redirect to original route

Performance Considerations

  • Route preloading for frequently accessed protected routes
  • Lazy loading of heavy components using React.lazy
  • Bundle analysis to ensure no route exceeds 500KB when split
  • Critical route loading prioritization (login, dashboard) over less-used routes

Security Considerations in Routing

  • All protected routes are validated server-side for role permissions
  • Route guards prevent direct URL access to unauthorized paths
  • Session timeout handling through route guard checks
  • Token refresh mechanisms integrated into navigation flow

Error Handling in Navigation

// components/NotFound.tsx
import { useNavigate } from 'react-router-dom';

export const NotFound = () => {
  const navigate = useNavigate();
  
  return (
    <div className="flex flex-col items-center justify-center min-h-screen">
      <h1 className="text-4xl font-bold">404</h1>
      <p>Page not found</p>
      <button 
        onClick={() => navigate('/')}
        className="mt-4 px-4 py-2 bg-blue-500 text-white rounded"
      >
        Go Home
      </button>
    </div>
  );
};

5. API Integration Layer

API Client Design Pattern

We'll implement a centralized API client using Axios with a custom wrapper to handle authentication, error handling, and request/response transformation. The client will be built as a singleton with configuration for base URL, timeout, and interceptors.

// api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';

class ApiClient {
  private client: AxiosInstance;

  constructor(baseURL: string) {
    this.client = axios.create({
      baseURL,
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json',
      },
    });

    this.setupInterceptors();
  }

  private setupInterceptors() {
    // Request interceptor for auth token injection
    this.client.interceptors.request.use(
      (config) => {
        const token = localStorage.getItem('authToken');
        if (token) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
      },
      (error) => Promise.reject(error)
    );

    // Response interceptor for error handling and token refresh
    this.client.interceptors.response.use(
      (response) => response,
      async (error) => {
        const originalRequest = error.config;
        
        if (error.response?.status === 401 && !originalRequest._retry) {
          originalRequest._retry = true;
          
          try {
            const newToken = await this.refreshAuthToken();
            localStorage.setItem('authToken', newToken);
            originalRequest.headers.Authorization = `Bearer ${newToken}`;
            return this.client(originalRequest);
          } catch (refreshError) {
            // Redirect to login if token refresh fails
            window.location.href = '/login';
            return Promise.reject(refreshError);
          }
        }
        
        return Promise.reject(error);
      }
    );
  }

  private async refreshAuthToken(): Promise<string> {
    const response = await axios.post('/api/auth/refresh', {
      refreshToken: localStorage.getItem('refreshToken'),
    });
    return response.data.token;
  }

  public get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.client.get<T>(url, config);
  }

  public post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.client.post<T>(url, data, config);
  }

  public put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.client.put<T>(url, data, config);
  }

  public delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.client.delete<T>(url, config);
  }
}

export const apiClient = new ApiClient(process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000');

Request/Response Interceptors

The interceptors handle:

  1. Authentication: Injecting Bearer tokens from localStorage
  2. Token Refresh: Handling 401 responses by refreshing auth tokens
  3. Error Normalization: Converting API errors to consistent format
  4. Response Transformation: Parsing and validating response data

Error Handling and Retry Strategy

// api/errorHandler.ts
import { AxiosError } from 'axios';

interface ApiError {
  code: string;
  message: string;
  status: number;
  timestamp: string;
}

export class ApiError extends Error {
  constructor(
    public readonly status: number,
    public readonly code: string,
    message: string
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

export const handleApiError = (error: AxiosError): ApiError => {
  if (error.response) {
    const { status, data } = error.response;
    return new ApiError(
      status,
      data?.code || 'UNKNOWN_ERROR',
      data?.message || `API Error: ${status}`
    );
  }
  
  if (error.request) {
    return new ApiError(0, 'NETWORK_ERROR', 'Network error occurred');
  }
  
  return new ApiError(0, 'UNKNOWN_ERROR', error.message);
};

// Retry strategy with exponential backoff
export const retryWithBackoff = async (
  fn: () => Promise<any>,
  retries = 3,
  delay = 1000
): Promise<any> => {
  for (let i = 0; i <= retries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === retries) throw error;
      
      // Exponential backoff with jitter
      const waitTime = delay * Math.pow(2, i) + Math.random() * 1000;
      await new Promise(resolve => setTimeout(resolve, waitTime));
    }
  }
};

Loading, Error, and Empty States Design

// api/types.ts
export interface ApiResponse<T> {
  data: T;
  meta?: {
    total?: number;
    page?: number;
    perPage?: number;
  };
}

export type LoadingState = 'idle' | 'loading' | 'success' | 'error';

export interface ApiResult<T> {
  data: T | null;
  loading: LoadingState;
  error: ApiError | null;
  refetch: () => void;
}

Data Fetching Strategy

For a modern React application, we'll use Client-Side Rendering (CSR) with React Query for data fetching:

// hooks/useApi.ts
import { useQuery, useMutation, QueryClient } from '@tanstack/react-query';
import { apiClient } from '../api/client';
import { ApiError, handleApiError } from '../api/errorHandler';

// Query client configuration
export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      cacheTime: 10 * 60 * 1000, // 10 minutes
      retry: 2,
      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
    },
  },
});

// Generic API hook
export function useApi<T>(
  key: string,
  endpoint: string,
  options?: {
    enabled?: boolean;
    staleTime?: number;
  }
) {
  const { data, isLoading, isError, error, refetch } = useQuery<T>({
    queryKey: [key, endpoint],
    queryFn: () => apiClient.get<T>(endpoint),
    enabled: options?.enabled,
    staleTime: options?.staleTime || 5 * 60 * 1000,
  });

  return {
    data: data || null,
    loading: isLoading ? 'loading' : 'success',
    error: isError ? handleApiError(error as any) : null,
    refetch,
  };
}

// Mutation hook for write operations
export function useApiMutation<T, D>(
  mutationKey: string,
  endpoint: string,
  method: 'post' | 'put' | 'delete' = 'post'
) {
  const mutation = useMutation<T, ApiError, D>({
    mutationKey: [mutationKey],
    mutationFn: async (data) => {
      const response = await apiClient[method](endpoint, data);
      return response;
    },
    onError: (error) => {
      console.error('API Error:', error);
    },
  });

  return {
    ...mutation,
    mutateAsync: mutation.mutateAsync,
    isLoading: mutation.isLoading,
  };
}

API Response Transformation Layer

// api/transformers.ts
export interface User {
  id: string;
  name: string;
  email: string;
  createdAt: string;
}

export interface UserApiResponse {
  id: string;
  name: string;
  email: string;
  created_at: string;
}

export const transformUserResponse = (response: UserApiResponse): User => ({
  id: response.id,
  name: response.name,
  email: response.email,
  createdAt: response.created_at,
});

// Generic transformer factory
export const createTransformer = <T, U>(transformer: (input: T) => U) => {
  return (data: T): U => transformer(data);
};

Sequence Diagram for Primary Data Flows

sequenceDiagram
    participant Client
    participant AuthInterceptor
    participant ApiClient
    participant Server
    participant TokenRefresh
    participant ErrorInterceptor

    Client->>ApiClient: Make request with token
    ApiClient->>AuthInterceptor: Inject auth header
    AuthInterceptor-->>ApiClient: Continue request
    ApiClient->>Server: Send request to API
    Server-->>ApiClient: Response (200 OK)
    ApiClient-->>Client: Return data

    Client->>ApiClient: Make request with expired token
    ApiClient->>AuthInterceptor: Inject auth header
    AuthInterceptor-->>ApiClient: Continue request
    ApiClient->>Server: Send request to API
    Server-->>ApiClient: Response (401 Unauthorized)
    ApiClient->>TokenRefresh: Request new token
    TokenRefresh-->>ApiClient: New valid token
    ApiClient->>Server: Retry request with new token
    Server-->>ApiClient: Response (200 OK)
    ApiClient-->>Client: Return data

Security Considerations in API Layer

  1. HTTPS Only: All API calls must be made over HTTPS to prevent man-in-the-middle attacks
  2. Token Security: Auth tokens are stored in localStorage with secure flags (HttpOnly cookies would be preferred but not always possible)
  3. Rate Limiting: API client implements retry logic with exponential backoff to prevent overwhelming the server
  4. Input Validation: All API responses are validated and transformed before being used in the application
  5. CORS Configuration: Server-side CORS policies are properly configured to only allow trusted origins

Caching Strategy

React Query provides built-in caching with:

  • Stale Time: 5 minutes before cache is considered stale
  • Cache Time: 10 minutes before cache is removed from memory
  • Automatic Refetching: Background refetching when window regains focus
  • Query Invalidation: Manual invalidation when data changes

Error Boundaries and Retry Logic

// components/ApiErrorBoundary.tsx
import React from 'react';

interface ApiErrorBoundaryProps {
  children: React.ReactNode;
}

interface ApiErrorBoundaryState {
  hasError: boolean;
  error?: Error;
}

class ApiErrorBoundary extends React.Component<ApiErrorBoundaryProps, ApiErrorBoundaryState> {
  constructor(props: ApiErrorBoundaryProps) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('API Error Boundary caught:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-container">
          <h2>Something went wrong with the API request</h2>
          <p>Please try again or contact support</p>
        </div>
      );
    }

    return this.props.children;
  }
}

export default ApiErrorBoundary;

6. Performance Design

Core Web Vitals Targets

  • Largest Contentful Paint (LCP): <2.5s for 95% of users
  • Interaction to Next Paint (INP): <200ms for 90% of interactions
  • Cumulative Layout Shift (CLS): <0.1 for 95% of users

Bundle Size Budget per Route

  • Home/Landing: <200KB (including vendor bundles)
  • Dashboard: <400KB (with charts, tables)
  • Profile/Settings: <150KB
  • API-Intensive Routes: <600KB (with heavy data visualization)

Critical Rendering Path Optimization

  • CSS: Inline critical CSS for above-the-fold content, async load remaining styles
  • JavaScript: Split bundles by route, defer non-critical scripts
  • HTML: Minimize DOM size, reduce render-blocking resources
  • Fonts: Preload critical font subsets, implement font fallbacks

Image Optimization Strategy

// Image optimization component with lazy loading and responsive sizing
const OptimizedImage = ({ 
  src, 
  alt, 
  width, 
  height,
  priority = false,
  sizes = "100vw"
}: {
  src: string;
  alt: string;
  width?: number;
  height?: number;
  priority?: boolean;
  sizes?: string;
}) => {
  return (
    <img
      src={src}
      alt={alt}
      width={width}
      height={height}
      loading={priority ? "eager" : "lazy"}
      sizes={sizes}
      srcSet={`
        ${src}?w=320 320w,
        ${src}?w=480 480w,
        ${src}?w=640 640w,
        ${src}?w=800 800w
      `}
      style={{ 
        objectFit: "cover",
        width: "100%",
        height: "auto"
      }}
    />
  );
};

Font Loading Strategy

// Font loading with preloading and fallbacks
const FontLoader = () => {
  return (
    <>
      <link 
        rel="preload" 
        href="/fonts/inter-regular.woff2" 
        as="font" 
        type="font/woff2" 
        crossOrigin="anonymous"
      />
      <style jsx global>{`
        @font-face {
          font-family: 'Inter';
          src: url('/fonts/inter-regular.woff2') format('woff2');
          font-display: swap;
        }
        
        body {
          font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
        }
      `}</style>
    </>
  );
};

Memoization Strategy

// Critical memoization patterns for performance
const ExpensiveComponent = React.memo(({ data, onAction }) => {
  const processedData = useMemo(() => {
    return data.map(item => ({
      ...item,
      computedValue: expensiveCalculation(item.value)
    }));
  }, [data]);

  const handleClick = useCallback((id) => {
    onAction(id);
  }, [onAction]);

  return (
    <div>
      {processedData.map(item => (
        <Item key={item.id} data={item} onClick={handleClick} />
      ))}
    </div>
  );
});

const Item = React.memo(({ data, onClick }) => {
  return (
    <button 
      onClick={() => onClick(data.id)}
      aria-label={`Action for ${data.name}`}
    >
      {data.name}
    </button>
  );
});

Virtualization for Large Lists

// Virtualized list implementation using react-window
import { FixedSizeList as List } from 'react-window';

const VirtualizedList = ({ items, itemHeight = 50 }) => {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].name}
    </div>
  );

  return (
    <List
      height={600}
      itemCount={items.length}
      itemSize={itemHeight}
      width="100%"
    >
      {Row}
    </List>
  );
};

Prefetching and Preloading Strategy

// Route-level prefetching with React Router
const PrefetchRoute = ({ path, component: Component }) => {
  const [isPrefetched, setIsPrefetched] = useState(false);
  
  useEffect(() => {
    if (!isPrefetched) {
      // Prefetch critical resources
      const prefetchResources = async () => {
        // Preload fonts, images, or API data
        const preloadPromises = [
          fetch('/api/user-data'),
          fetch('/fonts/inter-bold.woff2')
        ];
        
        await Promise.all(preloadPromises);
        setIsPrefetched(true);
      };
      
      prefetchResources();
    }
  }, [isPrefetched]);

  return <Component />;
};

// Prefetch strategy for API data
const usePrefetchData = (endpoint: string, options?: RequestInit) => {
  const [data, setData] = useState<any>(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const prefetch = async () => {
      setLoading(true);
      try {
        const response = await fetch(endpoint, options);
        const result = await response.json();
        setData(result);
      } catch (error) {
        console.error('Prefetch failed:', error);
      } finally {
        setLoading(false);
      }
    };

    prefetch();
  }, [endpoint, options]);

  return { data, loading };
};

Performance Monitoring Implementation

// Performance monitoring hook with RUM integration
const usePerformanceMonitoring = () => {
  useEffect(() => {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.entryType === 'largest-contentful-paint') {
          // Send LCP to monitoring service
          sendToRUM({
            type: 'lcp',
            value: entry.startTime,
            element: entry.element?.tagName || 'unknown'
          });
        }
      }
    });

    observer.observe({ entryTypes: ['largest-contentful-paint'] });
    
    return () => observer.disconnect();
  }, []);

  const sendToRUM = (metrics: any) => {
    // Implementation for sending to RUM service
    console.log('RUM Metrics:', metrics);
  };
};

Bundle Analysis and Optimization

// Bundle analysis configuration
const bundleAnalysis = {
  "sizeLimits": {
    "home": "200KB",
    "dashboard": "400KB",
    "profile": "150KB"
  },
  "optimizationStrategies": {
    "codeSplitting": {
      "routeBased": true,
      "componentBased": false
    },
    "lazyLoading": {
      "components": ["HeavyChart", "Modal"],
      "routes": ["Dashboard"]
    }
  }
};

Critical Performance Metrics Tracking

// Performance metrics tracking with error boundaries
const PerformanceTracker = () => {
  const [metrics, setMetrics] = useState({
    lcp: 0,
    inp: 0,
    cls: 0
  });

  useEffect(() => {
    // Track core web vitals
    if ('PerformanceObserver' in window) {
      const observer = new PerformanceObserver((entries) => {
        entries.getEntries().forEach((entry) => {
          switch (entry.entryType) {
            case 'largest-contentful-paint':
              setMetrics(prev => ({ ...prev, lcp: entry.startTime }));
              break;
            case 'layout-shift':
              setMetrics(prev => ({ ...prev, cls: entry.value }));
              break;
          }
        });
      });

      observer.observe({ entryTypes: ['largest-contentful-paint', 'layout-shift'] });
    }
  }, []);

  return <div className="performance-metrics">{JSON.stringify(metrics)}</div>;
};

7. Error Handling & Resilience

Error Boundary Placement Strategy

Error boundaries should be strategically placed to prevent cascading failures while maintaining user experience. The placement follows a hierarchical approach:

graph TD
    A[App Root] --> B[ErrorBoundary]
    B --> C[Feature Container]
    C --> D[Component Tree]
    D --> E[UI Component]
    D --> F[UI Component]
    D --> G[UI Component]

Key placement decisions:

  • App-level boundary: Catches unhandled errors in the entire application
  • Feature-level boundaries: Wrap individual feature modules (e.g., dashboard, user profile)
  • Component-level boundaries: Handle specific UI component failures (e.g., chart rendering errors)

Global Error Handler Design

A centralized error handler manages uncaught exceptions and logs them for monitoring:

// src/core/error/ErrorManager.ts
import { captureException } from '@sentry/browser';

interface ErrorContext {
  componentStack?: string;
  errorInfo?: any;
  metadata?: Record<string, any>;
}

class ErrorManager {
  private static instance: ErrorManager;
  
  static getInstance(): ErrorManager {
    if (!ErrorManager.instance) {
      ErrorManager.instance = new ErrorManager();
    }
    return ErrorManager.instance;
  }

  handleError(error: Error, context?: ErrorContext): void {
    // Log to console for dev
    if (process.env.NODE_ENV === 'development') {
      console.error('Unhandled error:', error, context);
    }

    // Send to monitoring service
    captureException(error, {
      contexts: {
        error: context,
      },
    });

    // Trigger fallback UI for user
    this.showUserFriendlyError(error, context);
  }

  showUserFriendlyError(error: Error, context?: ErrorContext): void {
    // Show toast or inline error message
    // This could be a global notification system
  }
}

export default ErrorManager.getInstance();

Offline / Network Failure Handling

Implement comprehensive offline resilience with automatic retry mechanisms:

// src/core/network/NetworkManager.ts
class NetworkManager {
  private static instance: NetworkManager;
  private isOnline = navigator.onLine;

  static getInstance(): NetworkManager {
    if (!NetworkManager.instance) {
      NetworkManager.instance = new NetworkManager();
    }
    return NetworkManager.instance;
  }

  constructor() {
    window.addEventListener('online', () => this.handleOnline());
    window.addEventListener('offline', () => this.handleOffline());
  }

  handleOnline(): void {
    this.isOnline = true;
    // Retry pending requests
    this.retryPendingRequests();
  }

  handleOffline(): void {
    this.isOnline = false;
    // Pause polling, queue requests
  }

  isNetworkAvailable(): boolean {
    return this.isOnline;
  }

  async retryPendingRequests(): Promise<void> {
    // Implementation for retrying queued requests
  }
}

Graceful Degradation Patterns

Implement fallback strategies for different failure scenarios:

// src/core/error/FallbackUI.tsx
import React from 'react';

interface FallbackProps {
  error?: Error;
  resetError?: () => void;
}

const FallbackUI: React.FC<FallbackProps> = ({ error, resetError }) => {
  return (
    <div className="fallback-container">
      <h2>Something went wrong</h2>
      {error && (
        <div className="error-details">
          <p>{error.message}</p>
          {process.env.NODE_ENV === 'development' && (
            <pre>{error.stack}</pre>
          )}
        </div>
      )}
      <button onClick={resetError} className="retry-button">
        Retry
      </button>
    </div>
  );
};

export default FallbackUI;

User Feedback on Errors

Implement multiple feedback mechanisms:

// src/core/error/ToastManager.ts
import { toast } from 'react-toastify';

class ToastManager {
  static showNetworkError(): void {
    toast.error('Network error. Please check your connection.', {
      position: "top-right",
      autoClose: 5000,
      hideProgressBar: false,
      closeOnClick: true,
      pauseOnHover: true,
      draggable: true,
    });
  }

  static showGenericError(message: string): void {
    toast.error(message, {
      position: "top-right",
      autoClose: 3000,
    });
  }

  static showSuccess(message: string): void {
    toast.success(message, {
      position: "top-right",
      autoClose: 2000,
    });
  }
}

export default ToastManager;

Error Boundary Implementation

// src/core/error/GlobalErrorBoundary.tsx
import React from 'react';
import FallbackUI from './FallbackUI';

interface ErrorBoundaryState {
  hasError: boolean;
  error?: Error;
}

class GlobalErrorBoundary extends React.Component<
  { children: React.ReactNode },
  ErrorBoundaryState
> {
  constructor(props: { children: React.ReactNode }) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
    // Log the error to monitoring service
    console.error('Error caught by boundary:', error, errorInfo);
  }

  render(): React.ReactNode {
    if (this.state.hasError) {
      return <FallbackUI error={this.state.error} />;
    }

    return this.props.children;
  }
}

export default GlobalErrorBoundary;

Retry Strategy Implementation

// src/core/network/RetryStrategy.ts
interface RetryConfig {
  maxRetries?: number;
  delay?: number;
  backoffMultiplier?: number;
}

class RetryStrategy {
  static async executeWithRetry<T>(
    operation: () => Promise<T>,
    config: RetryConfig = {}
  ): Promise<T> {
    const { maxRetries = 3, delay = 1000, backoffMultiplier = 2 } = config;
    let lastError: Error;

    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        return await operation();
      } catch (error) {
        lastError = error as Error;
        
        if (attempt === maxRetries) {
          throw lastError;
        }

        // Exponential backoff
        const waitTime = delay * Math.pow(backoffMultiplier, attempt);
        await this.delay(waitTime);
      }
    }

    throw lastError!;
  }

  private static delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

export default RetryStrategy;

Offline State Management

// src/core/network/OfflineManager.ts
import { useState, useEffect } from 'react';

const useOfflineStatus = () => {
  const [isOffline, setIsOffline] = useState(!navigator.onLine);

  useEffect(() => {
    const handleOnline = () => setIsOffline(false);
    const handleOffline = () => setIsOffline(true);

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return isOffline;
};

export default useOfflineStatus;

Comprehensive Error Handling Flow

stateDiagram-v2
    [*] --> CheckNetwork
    CheckNetwork --> Online : Network Available
    CheckNetwork --> Offline : No Network
    
    Online --> MakeRequest
    MakeRequest --> Success : Request Successful
    MakeRequest --> Retry : Request Failed
    
    Retry --> MaxRetriesReached : Max Attempts Reached
    Retry --> MakeRequest : Retry with Backoff
    
    MaxRetriesReached --> ShowError : Show User-Friendly Error
    Success --> ShowData : Display Data
    
    Offline --> QueueRequest : Queue for Later
    QueueRequest --> RetryWhenOnline : Retry on Reconnect
    
    ShowError --> UserAction : User Can Retry or Cancel
    UserAction --> MakeRequest : Retry Request

Error Recovery Patterns

  1. Automatic Retry: For transient network issues (5xx errors, timeouts)
  2. Manual Retry: For user-initiated actions (400 errors, validation failures)
  3. Offline Queueing: For requests that can be retried when network is restored
  4. Fallback Data Loading: Load cached or default data when API fails

Error Classification Strategy

// src/core/error/ErrorTypes.ts
enum ErrorType {
  NETWORK_ERROR = 'NETWORK_ERROR',
  AUTHENTICATION_ERROR = 'AUTHENTICATION_ERROR',
  VALIDATION_ERROR = 'VALIDATION_ERROR',
  SERVER_ERROR = 'SERVER_ERROR',
  CLIENT_ERROR = 'CLIENT_ERROR',
  OFFLINE_ERROR = 'OFFLINE_ERROR',
}

interface ApiError {
  type: ErrorType;
  code?: number;
  message: string;
  retryable?: boolean;
}

const classifyError = (error: any): ApiError => {
  if (error instanceof NetworkError) {
    return {
      type: ErrorType.NETWORK_ERROR,
      message: error.message,
      retryable: true,
    };
  }

  if (error.response?.status === 401) {
    return {
      type: ErrorType.AUTHENTICATION_ERROR,
      code: error.response.status,
      message: 'Authentication required',
      retryable: false,
    };
  }

  if (error.response?.status >= 500) {
    return {
      type: ErrorType.SERVER_ERROR,
      code: error.response.status,
      message: 'Server error occurred',
      retryable: true,
    };
  }

  return {
    type: ErrorType.CLIENT_ERROR,
    message: error.message || 'An unknown error occurred',
    retryable: false,
  };
};

Error Recovery Implementation

// src/core/error/RecoveryManager.ts
class RecoveryManager {
  static async handleNetworkError(error: Error): Promise<void> {
    if (this.isOffline()) {
      // Queue the request for later retry
      this.queueRequest(error);
    } else {
      // Show network error toast and offer retry
      ToastManager.showNetworkError();
    }
  }

  static isOffline(): boolean {
    return !navigator.onLine;
  }

  static queueRequest(error: Error): void {
    // Store in localStorage or IndexedDB for later retry
  }

  static async retryQueuedRequests(): Promise<void> {
    // Retry all queued requests when online
  }
}

export default RecoveryManager;

8. Testing Strategy

Unit Testing Approach

Component-Level Testing (70% of test coverage)

  • Test all presentational components with various prop combinations
  • Mock external dependencies (API calls, context providers)
  • Verify accessibility attributes and keyboard interactions
  • Test edge cases like empty states, loading states, error states

Utility Functions (20% of test coverage)

  • Test authentication token handling and refresh logic
  • Validate API client interceptor behavior
  • Verify data transformation utilities (date formatting, currency conversion)
  • Test error normalization and retry logic helpers

Hook Testing (10% of test coverage)

  • Test custom hooks that manage authentication state
  • Verify side-effect execution (token refresh, redirect logic)
  • Test hook return values and state transitions

Integration Testing Strategy

API Integration Tests (20% of integration coverage)

  • End-to-end flow testing for authentication and token refresh
  • Test request/response interception with mock server
  • Validate proper error handling and retry scenarios
  • Verify cache invalidation behavior in multi-tab scenarios

State Management Integration (15% of integration coverage)

  • Test global state updates and persistence across components
  • Validate optimistic update behavior with mock API responses
  • Verify proper cleanup of subscriptions and side effects

E2E Testing Scope

Core User Flows (80% of E2E coverage)

  • Authentication flow: login → token refresh → logout
  • Protected route access with proper redirect handling
  • Error state recovery: network failure → retry → success
  • Multi-tab session management and token invalidation

Testing Tools & Framework

// Playwright configuration for E2E testing
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  timeout: 30000,
  expect: {
    timeout: 5000
  },
  fullyParallel: true,
  reporter: [
    ['html'],
    ['json', { outputFile: 'test-results.json' }]
  ],
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'on-first-retry'
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] }
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] }
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] }
    }
  ]
});

Visual Regression Testing

Tool: Percy + Playwright

  • Test critical authentication and error UI states across browsers
  • Monitor for layout shifts in protected routes
  • Validate RTL language support in UI components

Accessibility Testing Automation

Framework: axe-core + Playwright

// Accessibility test example
import { test } from '@playwright/test';
import { checkA11y, injectAxe } from 'axe-playwright';

test('authentication form should be accessible', async ({ page }) => {
  await page.goto('/login');
  await injectAxe(page);
  await checkA11y(page, {
    rules: {
      'color-contrast': { enabled: false } // Allow for custom theme contrast
    }
  });
});

Test Data Strategy

Test Fixtures

  • Mock API responses with various status codes (200, 401, 403, 500)
  • Simulate network latency and connection failures
  • Create user role-based test scenarios (admin, regular user, guest)
  • Generate realistic but non-sensitive test data for authentication flows

Test Coverage Targets

Component Tests: 85% code coverage
Integration Tests: 70% of critical flows
E2E Tests: 95% of user journeys
Accessibility: 100% of interactive components

Test Environment Isolation

Parallel Test Execution

  • Separate test databases for each concurrent test run
  • Mock external services (auth server, payment gateway)
  • Isolated session storage and local storage per test
  • Use in-memory cache for consistent test results

Test Data Cleanup

  • Automatically clean up test users and sessions after each test
  • Reset global state between test suites
  • Ensure no cross-contamination between authentication and data flows

9. Accessibility (A11y)

WCAG 2.1 AA compliance checklist for this feature

The API communication layer must ensure accessibility compliance through:

  • Keyboard Navigation: All interactive elements (API call buttons, retry actions, error modals) must be fully operable via keyboard
  • Screen Reader Support: ARIA labels and roles must properly announce API status changes (loading, success, error states)
  • Focus Management: Proper focus trapping in modals and error displays to prevent keyboard navigation from escaping
  • Contrast Ratios: Error messages and status indicators must meet 4.5:1 contrast ratio against backgrounds
  • Alternative Text: Loading spinners and status indicators must have ARIA labels for screen reader users

Keyboard navigation support

All API interaction elements must be keyboard accessible:

// Example: Accessible API status component
const ApiStatusIndicator = ({ status, error }: { status: 'idle' | 'loading' | 'success' | 'error'; error?: string }) => {
  const [isFocused, setIsFocused] = useState(false);
  
  return (
    <div 
      className={`api-status ${status}`}
      role="status"
      aria-live="polite"
      tabIndex={0}
      onFocus={() => setIsFocused(true)}
      onBlur={() => setIsFocused(false)}
    >
      {status === 'loading' && (
        <span aria-label="API request in progress">
          <Spinner />
        </span>
      )}
      {status === 'error' && (
        <span 
          role="alert"
          aria-live="assertive"
          className="error-message"
        >
          {error}
        </span>
      )}
    </div>
  );
};

Focus management strategy

Focus trapping in error modals and status displays:

// Focus trap implementation for API error dialogs
const useFocusTrap = (isOpen: boolean) => {
  const modalRef = useRef<HTMLDivElement>(null);
  
  useEffect(() => {
    if (!isOpen) return;
    
    const focusableElements = modalRef.current?.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    ) || [];
    
    const firstElement = focusableElements[0] as HTMLElement;
    const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
    
    const handleFocus = (e: FocusEvent) => {
      if (!modalRef.current?.contains(e.target as Node)) {
        lastElement.focus();
      }
    };
    
    firstElement?.focus();
    
    return () => {
      // Restore focus to previous element or body
      document.activeElement?.blur();
    };
  }, [isOpen]);
};

ARIA roles and attributes plan

API communication components will use:

  • role="status" for loading indicators
  • aria-live="polite" for status updates that don't require immediate attention
  • aria-live="assertive" for critical error messages
  • role="alert" for modal error displays
  • aria-label for icon-only status indicators

Screen reader compatibility

All API status changes will be announced to screen readers:

// Screen reader accessible status updates
const ApiStatusAnnouncer = ({ status, message }: { status: string; message?: string }) => {
  return (
    <div 
      className="sr-only"
      aria-live={status === 'error' ? 'assertive' : 'polite'}
      aria-atomic="true"
    >
      {status === 'loading' && 'API request in progress'}
      {status === 'success' && 'API request completed successfully'}
      {status === 'error' && `API error: ${message}`}
    </div>
  );
};

Keyboard navigation and focus restoration

API interaction elements will support:

  • Tab cycling through API controls
  • Enter/Space key activation for actions
  • Escape key to close error modals
  • Proper focus restoration when modals close

Color contrast compliance

All API status indicators will meet:

  • 4.5:1 contrast ratio for normal text
  • 3:1 contrast ratio for large text (18pt+)
  • No reliance on color alone to convey meaning

Alternative text and semantic structure

API status elements will include:

  • Semantic HTML structure (divs with appropriate roles)
  • Descriptive ARIA labels for icon-only elements
  • Proper heading hierarchy for error messages
  • Logical reading order for status information

Dynamic content updates

API status changes will be announced:

// Dynamic status announcement pattern
const StatusAnnouncer = ({ status, message }: { status: 'loading' | 'success' | 'error'; message?: string }) => {
  const [announced, setAnnounced] = useState(false);
  
  useEffect(() => {
    if (status !== 'idle' && !announced) {
      // Announce to screen readers
      const statusElement = document.createElement('div');
      statusElement.className = 'sr-only';
      statusElement.setAttribute('aria-live', 'assertive');
      statusElement.textContent = 
        status === 'loading' ? 'Request in progress' :
        status === 'success' ? 'Request completed successfully' :
        `Request failed: ${message}`;
      
      document.body.appendChild(statusElement);
      
      // Cleanup after announcement
      setTimeout(() => {
        document.body.removeChild(statusElement);
        setAnnounced(true);
      }, 1000);
    }
  }, [status, message]);
  
  return null;
};

Testing considerations

Accessibility testing will include:

  • Automated axe-core tests for WCAG compliance
  • Manual keyboard navigation testing
  • Screen reader testing with NVDA, JAWS, and VoiceOver
  • Focus order validation in all API interaction flows

10. Security Considerations

XSS Prevention Strategy

  • Input sanitization: All user-provided content must be sanitized using DOMPurify or similar library before rendering to prevent script injection
  • Content Security Policy (CSP): Implement strict CSP headers to block inline scripts and unsafe eval
  • Template literal escaping: Never directly interpolate user input into HTML templates or DOM manipulation methods
  • React's built-in protection: Leverage React's automatic escaping of text content (not JSX attributes or event handlers)

CSRF Protection

  • CSRF token injection: Include CSRF tokens in all state-modifying requests (POST, PUT, DELETE)
  • Token storage: Store CSRF tokens in HTTP-only cookies or secure session storage with proper expiration
  • Token validation: Server-side validation of CSRF tokens on every sensitive operation
  • SameSite cookies: Set SameSite=Strict or SameSite= Lax on authentication cookies to prevent cross-site request forgery

Content Security Policy (CSP) Headers

Content-Security-Policy: 
  default-src 'self'; 
  script-src 'self' 'unsafe-inline' 'unsafe-eval'; 
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:; 
  font-src 'self' data:;
  connect-src 'self' wss:;
  frame-ancestors 'none';

Sensitive Data Handling in Frontend State

  • Token storage: Never store authentication tokens in localStorage or sessionStorage - use secure HTTP-only cookies instead
  • Memory cleanup: Clear sensitive data from memory immediately after use (e.g., password fields, auth tokens)
  • State obfuscation: Mask sensitive data in debug tools (e.g., show "••••" instead of actual passwords)
  • Console security: Disable console logging in production or strip sensitive data from logs

Input Sanitization Approach

// Example of input sanitization for API communication
const sanitizeInput = (input: string): string => {
  // Remove potentially dangerous characters
  const sanitized = input.replace(/[<>'"&]/g, (char) => {
    switch (char) {
      case '<': return '&lt;';
      case '>': return '&gt;';
      case "'": return '&#x27;';
      case '"': return '&quot;';
      case '&': return '&amp;';
      default: return char;
    }
  });
  
  // Remove JavaScript event handlers
  return sanitized.replace(/on\w+\s*=\s*["'][^"']*["']/gi, '');
};

// API request with sanitized inputs
const sendSecureRequest = async (endpoint: string, data: any) => {
  const sanitizedData = {
    ...data,
    message: sanitizeInput(data.message),
    title: sanitizeInput(data.title)
  };
  
  return fetch(endpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': getCSRFToken(),
      'X-Request-ID': generateRequestId()
    },
    body: JSON.stringify(sanitizedData)
  });
};

Security Headers Implementation

// Server-side security headers (example in Express.js)
app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('X-XSS-Protection', '1; mode=block');
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  res.setHeader('Content-Security-Policy', CSP_POLICY);
  next();
});

Authentication Token Security

  • Secure cookie flags: Secure, HttpOnly, SameSite=Strict for authentication cookies
  • Token refresh: Implement automatic token refresh with proper expiration handling
  • Token revocation: Support immediate token invalidation on logout or session timeout
  • Cross-site protection: Prevent token leakage between different domains or subdomains

Rate Limiting and Throttling

  • Request rate limiting: Implement client-side request throttling to prevent abuse
  • API endpoint limits: Enforce rate limits on sensitive API endpoints server-side
  • IP-based blocking: Track and block suspicious IP addresses or patterns of abuse

Security Testing Considerations

  • Vulnerability scanning: Integrate automated security scanning in CI pipeline (e.g., Snyk, OWASP ZAP)
  • Penetration testing: Regular manual security audits of API endpoints
  • Security code review: Include security checks in pull request reviews for all API communication logic

Key Security Risks Mitigated

  1. Cross-site scripting (XSS): Through input sanitization and CSP implementation
  2. Cross-site request forgery (CSRF): Via CSRF token validation and SameSite cookies
  3. Data exposure: Through secure storage of authentication tokens and sensitive data handling
  4. API abuse: Through rate limiting and request validation mechanisms

11. Internationalization (i18n)

Translation Key Structure and Naming Convention

Following BEM-style naming with feature-scoped keys to prevent collisions:

// Translation keys organized by feature and context
{
  "auth": {
    "login": {
      "title": "Login to your account",
      "username_label": "Username",
      "password_label": "Password",
      "submit_button": "Sign In"
    },
    "register": {
      "title": "Create new account",
      "email_label": "Email Address",
      "confirm_password_label": "Confirm Password"
    }
  },
  "dashboard": {
    "welcome_message": "Welcome back, {{name}}!",
    "stats": {
      "total_users": "Total Users: {{count}}",
      "revenue": "Revenue: {{amount}}"
    }
  },
  "common": {
    "loading": "Loading...",
    "error": "An error occurred",
    "cancel": "Cancel",
    "save": "Save"
  }
}

RTL Support Considerations

RTL layout handling through CSS classes and direction properties:

/* Global RTL utility */
[dir="rtl"] .text-left {
  text-align: right;
}

[dir="rtl"] .float-start {
  float: right;
}

/* Component-specific RTL handling */
.rtl-layout {
  direction: rtl;
  text-align: right;
}

.rtl-flex-row-reverse {
  flex-direction: row-reverse;
}

Date, Number, Currency Formatting Strategy

Using Intl API with fallbacks for unsupported locales:

// i18n formatting utilities
const formatCurrency = (amount: number, locale: string, currency: string = 'USD') => {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency,
    minimumFractionDigits: 2,
    maximumFractionDigits: 2
  }).format(amount);
};

const formatDate = (date: Date, locale: string, options?: Intl.DateTimeFormatOptions) => {
  return new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: 'short',
    day: 'numeric',
    ...options
  }).format(date);
};

const formatNumber = (num: number, locale: string) => {
  return new Intl.NumberFormat(locale).format(num);
};

Locale Detection and Switching Strategy

Automatic browser locale detection with manual override:

// i18n context and provider
type Locale = 'en-US' | 'fr-FR' | 'ar-SA' | 'zh-CN';

interface I18nContextType {
  locale: Locale;
  setLocale: (locale: Locale) => void;
  t: (key: string, params?: Record<string, any>) => string;
  formatDate: (date: Date, options?: Intl.DateTimeFormatOptions) => string;
  formatCurrency: (amount: number, currency?: string) => string;
}

const I18nContext = createContext<I18nContextType>({
  locale: 'en-US',
  setLocale: () => {},
  t: () => '',
  formatDate: () => '',
  formatCurrency: () => ''
});

// Auto-detect locale from browser or user preference
const detectLocale = (): Locale => {
  const savedLocale = localStorage.getItem('locale');
  if (savedLocale && isValidLocale(savedLocale)) return savedLocale as Locale;
  
  const browserLocale = navigator.language;
  if (isValidLocale(browserLocale)) return browserLocale as Locale;
  
  return 'en-US';
};

// Valid locale validation
const isValidLocale = (locale: string): boolean => {
  const validLocales = ['en-US', 'fr-FR', 'ar-SA', 'zh-CN'];
  return validLocales.includes(locale);
};

Translation Key Management

Translation files organized by locale and feature:

// translations/en-US.json
{
  "auth": {
    "login": {
      "title": "Sign In to Your Account",
      "username_label": "Username",
      "password_label": "Password",
      "submit_button": "Sign In"
    }
  },
  "common": {
    "loading": "Loading...",
    "error": "An error occurred"
  }
}

// translations/ar-SA.json
{
  "auth": {
    "login": {
      "title": "تسجيل الدخول إلى حسابك",
      "username_label": "اسم المستخدم",
      "password_label": "كلمة المرور",
      "submit_button": "تسجيل الدخول"
    }
  },
  "common": {
    "loading": "جاري التحميل...",
    "error": "حدث خطأ"
  }
}

i18n Provider Implementation

// i18n provider with fallback handling
const I18nProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [locale, setLocale] = useState<Locale>(detectLocale());
  const [translations, setTranslations] = useState<Record<string, any>>({});

  useEffect(() => {
    const loadTranslations = async () => {
      try {
        const translationModule = await import(`../locales/${locale}.json`);
        setTranslations(translationModule.default);
      } catch (error) {
        console.error(`Failed to load translations for ${locale}`, error);
        // Fallback to English
        const fallbackModule = await import('../locales/en-US.json');
        setTranslations(fallbackModule.default);
      }
    };

    loadTranslations();
  }, [locale]);

  const t = useCallback((key: string, params?: Record<string, any>): string => {
    const keys = key.split('.');
    let translation = translations;
    
    for (const k of keys) {
      translation = translation?.[k];
      if (!translation) return key;
    }

    if (typeof translation === 'string' && params) {
      return translation.replace(/\{\{(\w+)\}\}/g, (_, param) => params[param] || `{{${param}}}`);
    }

    return translation;
  }, [translations]);

  const value = useMemo(() => ({
    locale,
    setLocale,
    t,
    formatDate: (date: Date, options?: Intl.DateTimeFormatOptions) => 
      new Intl.DateTimeFormat(locale, options).format(date),
    formatCurrency: (amount: number, currency?: string) => 
      new Intl.NumberFormat(locale, {
        style: 'currency',
        currency: currency || 'USD'
      }).format(amount)
  }), [locale, t]);

  return (
    <I18nContext.Provider value={value}>
      {children}
    </I18nContext.Provider>
  );
};

Component Integration Example

// i18n-aware component using context
const LoginButton: React.FC = () => {
  const { t, setLocale } = useContext(I18nContext);
  
  const handleLocaleChange = (newLocale: Locale) => {
    setLocale(newLocale);
    localStorage.setItem('locale', newLocale);
  };

  return (
    <div>
      <h1>{t('auth.login.title')}</h1>
      <input 
        type="text" 
        placeholder={t('auth.login.username_label')} 
      />
      <input 
        type="password" 
        placeholder={t('auth.login.password_label')} 
      />
      <button>{t('auth.login.submit_button')}</button>
      
      <div className="locale-switcher">
        <button onClick={() => handleLocaleChange('en-US')}>EN</button>
        <button onClick={() => handleLocaleChange('ar-SA')}>AR</button>
      </div>
    </div>
  );
};

12. Monitoring & Observability

Real User Monitoring (RUM) Metrics to Track

The frontend will implement comprehensive RUM tracking to monitor user experience and application performance:

  • Largest Contentful Paint (LCP): Target <2.5s for 95% of users
  • First Input Delay (FID): Target <100ms for 90% of users
  • Cumulative Layout Shift (CLS): Target <0.1 for 90% of users
  • Time to First Byte (TTFB): Target <200ms for 95% of requests
  • Page Load Time: Target <3s for 90% of users
  • Error Rate: <0.1% of page views should result in unhandled errors
  • API Response Time: <200ms for 95% of requests

Error Tracking Integration

Sentry will be the primary error tracking solution with:

  • Global Error Boundaries: Capture unhandled promise rejections and uncaught exceptions
  • Error Context: Include user session, browser info, URL path, and component stack traces
  • Performance Context: Attach timing data for slow API calls and long-running operations
  • Sample Rate: 10% of errors for production, 100% for staging

Custom Events and User Journey Tracking

User Action Events will be instrumented to understand feature adoption:

// Event tracking interface
interface UserEvent {
  eventName: string;
  properties?: Record<string, any>;
  timestamp: number;
}

// Example event tracking for API interactions
const trackApiEvent = (event: UserEvent) => {
  // Send to analytics backend or directly to RUM service
  const payload = {
    ...event,
    userId: getCurrentUserId(),
    sessionId: getSessionId(),
    timestamp: Date.now(),
  };
  
  // Send to monitoring service
  if (window.RUM_SERVICE) {
    window.RUM_SERVICE.trackEvent(payload);
  }
};

User Journey Tracking will capture:

  • API call sequences (request → response → error)
  • Feature usage patterns (e.g., "user completed form flow")
  • Performance regressions over time

Performance Monitoring and Regression Detection

Performance Baseline Tracking:

  • Store historical performance metrics in local storage for regression detection
  • Alert on >10% degradation from baseline for key metrics (LCP, FID)
  • Implement automated performance testing in CI pipeline

Automated Regression Testing:

// Performance monitoring hook
const usePerformanceMonitor = () => {
  const [metrics, setMetrics] = useState<PerformanceMetrics>({
    lcp: 0,
    fid: 0,
    cls: 0,
    ttfb: 0
  });

  useEffect(() => {
    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        if (entry.entryType === 'largest-contentful-paint') {
          setMetrics(prev => ({ ...prev, lcp: entry.startTime }));
        }
        if (entry.entryType === 'first-input') {
          setMetrics(prev => ({ ...prev, fid: entry.processingStart - entry.startTime }));
        }
      });
    });

    observer.observe({ entryTypes: ['largest-contentful-paint', 'first-input'] });
    
    return () => observer.disconnect();
  }, []);

  return metrics;
};

Threshold-based Alerts:

  • LCP > 2.5s triggers alert
  • FID > 100ms triggers alert
  • CLS > 0.1 triggers alert
  • API response time > 200ms triggers alert

Integration with Backend Monitoring

Cross-System Correlation:

  • Include request IDs in frontend logs to trace requests through backend services
  • Send user session IDs and feature flags for cross-system debugging
  • Implement distributed tracing with OpenTelemetry-compatible headers

Log Aggregation Strategy:

  • Frontend logs will be sent to centralized logging service (e.g., ELK stack)
  • Structured logging format with consistent field naming
  • Log levels: ERROR, WARN, INFO, DEBUG (DEBUG only in dev/staging)

Dashboard and Alerting Strategy

Monitoring Dashboard:

  • Real-time performance metrics dashboard
  • Error rate trends by feature and user segment
  • API response time heatmaps
  • User journey completion rates

Alerting Configuration:

  • Slack/Teams alerts for >5% error rate in production
  • Email notifications for >20% performance degradation
  • Incident response workflow integration with PagerDuty or similar

Data Privacy and Compliance Considerations

Data Minimization:

  • Only collect necessary performance data for monitoring
  • Anonymize user identifiers in logs and metrics
  • Implement opt-out mechanisms for users who prefer not to be tracked

Compliance with Regulations:

  • GDPR compliance for EU users (data processing consent)
  • CCPA compliance for California users (right to know and delete)
  • HIPAA considerations for healthcare applications (if applicable)

Observability Tooling Stack

Primary Tools:

  • Sentry: Error tracking and performance monitoring
  • DataDog/Logz.io: Log aggregation and analysis
  • New Relic/AppDynamics: Full-stack application performance monitoring
  • Lighthouse CI: Automated accessibility and performance testing in CI

Custom Metrics Pipeline:

// Example of custom metric collection
class MetricsCollector {
  private metrics: Record<string, any> = {};
  
  collectPerformanceMetric(name: string, value: number) {
    this.metrics[name] = {
      value,
      timestamp: Date.now(),
      userAgent: navigator.userAgent,
      viewport: `${window.innerWidth}x${window.innerHeight}`
    };
    
    // Send to monitoring service
    this.sendToRUMService(this.metrics);
  }
  
  sendToRUMService(metrics: Record<string, any>) {
    // Implementation for sending to RUM service
    fetch('/api/monitoring/metrics', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(metrics)
    });
  }
}

This monitoring strategy provides comprehensive visibility into both user experience and system performance while maintaining data privacy and compliance standards. The approach balances detailed observability with minimal impact on application performance and user experience.

13. File & Folder Structure

Proposed Directory Structure

/src
  /api
    /clients
      apiClient.ts
      authClient.ts
      index.ts
    /interceptors
      authInterceptor.ts
      errorInterceptor.ts
      responseInterceptor.ts
    /types
      index.ts
      apiTypes.ts
    index.ts
  /components
    /common
      Button/
        Button.tsx
        Button.stories.tsx
        index.ts
      Modal/
        Modal.tsx
        index.ts
      index.ts
    /features
      Dashboard/
        Dashboard.tsx
        Dashboard.stories.tsx
        index.ts
      UserList/
        UserList.tsx
        UserList.stories.tsx
        index.ts
      index.ts
    index.ts
  /hooks
    useApi.ts
    useAuth.ts
    index.ts
  /store
    authSlice.ts
    index.ts
  /utils
    logger.ts
    validators.ts
    index.ts
  /types
    index.ts
    globalTypes.ts
  /services
    authService.ts
    userService.ts
    index.ts
  /contexts
    AuthContext.tsx
    index.ts
  /routes
    index.ts
    ProtectedRoute.tsx
  /styles
    globals.css
    theme.ts
  /assets
    images/
    icons/
  App.tsx
  index.tsx

Module Boundaries and Import Rules

  1. Feature isolation: Each feature folder contains all related components, hooks, and services
  2. Shared components: Common UI elements live in /components/common with barrel exports
  3. No cross-feature imports: Components within features cannot directly import from other features
  4. Barrel exports: Each feature exports a clean public API via index.ts files
  5. Layered architecture: API layer is isolated from components, services abstract API calls

Shared vs Feature-Specific Code Separation

Shared code (src/):

  • Common UI components
  • Global types and interfaces
  • Utility functions and helpers
  • Authentication context and services
  • API client configuration

Feature-specific code (src/features/):

  • Component implementations
  • Feature-specific hooks and services
  • Storybook stories and tests
  • Feature-level state management

This structure enables:

  • Clear ownership boundaries
  • Easy code reuse across features
  • Isolated testing per feature
  • Scalable architecture for growing teams and features

14. Dependencies & Third-Party Libraries

Key Libraries Selected with Justification

axios - Primary HTTP client for API communication due to:

  • Built-in support for request/response interceptors (critical for auth token management)
  • Automatic JSON parsing and error handling
  • Promise-based API that integrates well with React's async patterns
  • Strong ecosystem of plugins and middleware support

react-query - State management for server state with:

  • Built-in caching, invalidation, and refetching strategies
  • Automatic deduplication of requests
  • Server state synchronization with client state
  • Excellent TypeScript support and type inference

zod - Schema validation for API responses with:

  • Compile-time type safety and runtime validation
  • Clean, declarative API that reduces boilerplate
  • Integration with TypeScript for seamless type generation
  • Zero runtime overhead in production builds

jose - JWT handling and cryptographic operations with:

  • RFC-compliant JWT parsing and validation
  • Built-in support for JWK sets and key rotation
  • Lightweight implementation suitable for browser environments
  • Strong TypeScript typing

lodash - Utility library for common operations with:

  • Optimized implementations of common functional patterns
  • Tree-shaking friendly - only imported functions are bundled
  • Consistent, well-tested APIs that reduce implementation risk

Alternatives Considered and Rejected

ky - Rejected due to:

  • Less mature ecosystem compared to axios
  • Limited interceptor support for complex auth flows
  • Smaller community and fewer third-party integrations

swr - Rejected due to:

  • Less flexible caching strategies than react-query
  • More complex configuration for custom cache invalidation
  • Less mature support for server-side rendering scenarios

superagent - Rejected due to:

  • Less active maintenance and community support
  • More verbose API compared to axios
  • Limited built-in features for authentication flows

Licensing and Bundle Size Impact Assessment

axios (1.6.0) - MIT licensed, ~25KB minified. Critical for core API communication flow.

react-query (5.24.0) - MIT licensed, ~15KB minified. Essential for server state management.

zod (3.22.4) - MIT licensed, ~12KB minified. Critical for type-safe API validation.

jose (4.15.4) - MIT licensed, ~20KB minified. Required for JWT handling and security.

lodash (4.17.21) - MIT licensed, ~25KB minified. Used for utility functions with minimal impact on bundle size.

Bundle Size Budget Allocation

  • Core HTTP stack (axios + jose): ~45KB
  • State management (react-query + zod): ~27KB
  • Utility functions (lodash): ~25KB
  • Total: ~97KB for secure API communication layer

Security-Centric Library Selection

All selected libraries undergo security audits and have active maintenance. JWT handling with jose ensures cryptographic best practices, while axios interceptors provide secure token management without exposing sensitive credentials in client-side code.

Feature-Specific Library Usage

axios interceptors handle automatic token refresh, request/response transformation, and security headers injection. react-query manages caching strategies for authenticated endpoints with proper invalidation when tokens expire.

// Example of secure API client configuration
import axios from 'axios';
import { jwtVerify } from 'jose';

const apiClient = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
  timeout: 10000,
});

apiClient.interceptors.request.use(
  async (config) => {
    const token = localStorage.getItem('authToken');
    if (token) {
      // Validate token before sending
      try {
        await jwtVerify(token, new TextEncoder().encode(process.env.JWT_SECRET));
        config.headers.Authorization = `Bearer ${token}`;
      } catch (error) {
        // Handle expired or invalid tokens
        localStorage.removeItem('authToken');
        throw new Error('Invalid authentication token');
      }
    }
    return config;
  },
  (error) => Promise.reject(error)
);

15. Open Questions, Risks & Trade-offs

Decisions that need product/engineering alignment

  • Authentication flow strategy: Should we implement OAuth 2.0 with refresh tokens or JWT-based stateless authentication? This decision impacts session management, token storage strategy, and cross-origin support requirements.
  • API versioning approach: Should we use header-based versioning, path-based versioning, or content negotiation? This affects client compatibility and deployment strategies.
  • Rate limiting policy: What are the specific rate limits per endpoint and how should they be enforced (per user, per IP, or per application)? This impacts user experience and abuse prevention.
  • Data retention and deletion policies: How long should API tokens and session data be retained before automatic cleanup? This affects security posture and compliance requirements.

Known trade-offs made in this design

  • Security vs. performance: Implementing comprehensive request validation and logging adds CPU overhead but provides better security coverage. We've chosen to implement selective validation for high-traffic endpoints while maintaining full validation for sensitive operations.
  • Flexibility vs. security: Using a centralized API gateway provides better security controls but reduces flexibility in handling custom authentication flows or per-route configurations. We've opted for a hybrid approach where security-critical endpoints use the gateway while others maintain direct integration.
  • Developer experience vs. security: Providing extensive API documentation and SDKs improves developer adoption but increases attack surface. We've implemented rate limiting and authentication requirements to balance these concerns.
  • Real-time vs. batch processing: Supporting real-time API responses requires more complex infrastructure and increases latency variability. We've chosen to prioritize batch processing for better predictability while providing real-time capabilities where required.

Risks and mitigations

  • Token leakage risk: If API tokens are improperly stored or transmitted, they could be leaked. Mitigation: Implement strict token rotation policies, enforce HTTPS-only transmission, and use secure HTTP-only cookies for session tokens.
  • DDoS attack vulnerability: High-volume API endpoints could be targeted for DDoS attacks. Mitigation: Implement automatic rate limiting, request queuing, and integration with cloud WAF services.
  • Data exposure risk: Sensitive data in API responses could be exposed due to improper filtering. Mitigation: Implement comprehensive response sanitization, field-level access control, and audit logging for all sensitive data access.
  • Cross-origin resource sharing (CORS) misconfiguration: Improper CORS configuration could allow unauthorized domains to access the API. Mitigation: Implement strict CORS policies with specific allowed origins and enforce preflight request validation.

Technical debt acknowledged

  • Legacy API support: Maintaining backward compatibility with older API versions introduces technical debt through code duplication and complexity in request routing. This will require ongoing maintenance efforts as we migrate to newer versions.
  • Inconsistent error handling: Different API endpoints may return different error response formats, creating inconsistency in client-side error handling. We've documented the expected format but haven't yet standardized all responses.
  • Limited audit logging: Current implementation provides basic request logging but lacks comprehensive audit trail capabilities for compliance and security monitoring. This will require additional development effort in future iterations.
  • Manual security review process: Security checks are currently performed manually during code reviews rather than automated. This creates a potential gap in security coverage and requires ongoing human resource commitment for security validation.

Was this article helpful?

Topics in this article

More from Frontend LLD

  • Frontend LLD

    Automatic Token Refresh System Design

    Comprehensive low-level design for implementing automatic JWT token refresh in frontend applications with error handling and concurrency control

  • Frontend LLD

    JWT Token Management System Design

    Comprehensive low-level design for secure JWT token handling, refresh mechanisms, and session management in modern web applications.

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.