Skip to main content

Comprehensive CSRF Protection Mechanisms Design

Detailed low-level design for implementing robust Cross-Site Request Forgery protection in modern web applications with multiple mitigation strategies.

Cross-Site Request Forgery represents one of the most critical security vulnerabilities in web applications, requiring multi-layered protection mechanisms that combine token-based validation, origin checking, and request integrity verification to prevent unauthorized actions on behalf of authenticated users.

1. Problem Statement & Requirements

Clarifying questions

  1. What's the target browser support matrix? Do we need to support legacy browsers like IE11 or older Edge versions?
  2. Is this feature part of a larger application that already has authentication and session management in place, or is it a standalone feature?
  3. Are there any specific real-time requirements for CSRF protection (e.g., live forms, auto-saved drafts)?
  4. Will this feature support offline scenarios where tokens might expire or need regeneration?
  5. Do we have any accessibility requirements that might impact how CSRF tokens are handled (e.g., screen reader compatibility)?
  6. Is there a design system in place that we should integrate with for token display or error handling?
  7. What's the expected user journey flow? Is this a single-page application with dynamic route changes or a multi-page form?

Functional requirements

  • Generate and embed CSRF tokens in forms and API requests
  • Validate CSRF tokens server-side for all state-modifying operations (POST, PUT, DELETE)
  • Automatically refresh or regenerate tokens when they expire or become invalid
  • Support both form-based and AJAX-based API requests with CSRF token inclusion
  • Provide fallback mechanisms for token regeneration in case of invalid tokens
  • Display user-friendly error messages when CSRF validation fails
  • Implement token storage that persists across page refreshes but is cleared on logout

Non-functional requirements

  • Security: CSRF tokens must be cryptographically secure and tied to user sessions
  • Performance: Token generation and validation should not introduce noticeable latency
  • Reliability: Token handling must be resilient to network failures and session timeouts
  • Scalability: Token management should scale with concurrent user sessions
  • Compatibility: Must work across different frontend frameworks and deployment environments

Constraints and assumptions

  • CSRF tokens are generated server-side and must be securely transmitted to the client
  • Token validation assumes proper session management and user authentication
  • Client-side token storage is assumed to be secure (e.g., HTTP-only cookies or secure local storage)
  • The application has a centralized authentication and session management system
  • Server-side implementation handles token regeneration and invalidation logic

Out of scope

  • Token storage in browser cookies (this is handled by the authentication system)
  • Session timeout handling (handled by existing session management)
  • Token encryption or obfuscation (assumed to be handled by server-side security)
  • Cross-origin request protection (handled by CORS policies and backend validation)
  • Token revocation for specific users or sessions (handled by session management system)
  • Token expiration time configuration (handled by backend security policies)

2. Component Architecture

graph TD
    A[CSRFProtectionProvider] --> B[CSRFTokenManager]
    A --> C[AuthContext]
    B --> D[CSRFTokenInput]
    B --> E[CSRFTokenHeader]
    C --> F[ProtectedRoute]
    F --> G[ProtectedComponent]
    F --> H[AuthRedirect]

Component Tree and Responsibilities

The CSRF protection system is built as a provider pattern that wraps the application and manages token lifecycle. The component architecture includes:

  1. CSRFProtectionProvider - Root provider that initializes and manages the CSRF token lifecycle
  2. CSRFTokenManager - Internal component that handles token fetching, storage, and injection
  3. CSRFTokenInput - Hidden form field component for token inclusion in forms
  4. CSRFTokenHeader - HTTP header injection component for API requests
  5. ProtectedRoute - Route guard that ensures CSRF tokens are present before allowing protected routes
  6. AuthContext - Authentication context that integrates with CSRF protection

TypeScript Props Contracts

// CSRFProtectionProvider props
interface CSRFProtectionProviderProps {
  children: React.ReactNode;
  tokenUrl?: string;
  tokenHeaderName?: string;
  tokenStorageKey?: string;
  refreshThreshold?: number; // seconds before token expiry to refresh
}

// CSRFTokenManager props
interface CSRFTokenManagerProps {
  tokenUrl?: string;
  tokenHeaderName?: string;
  tokenStorageKey?: string;
  refreshThreshold?: number;
}

// CSRFTokenInput props
interface CSRFTokenInputProps {
  name?: string;
  value?: string;
}

// CSRFTokenHeader props
interface CSRFTokenHeaderProps {
  tokenName?: string;
  tokenValue?: string;
}

UI State Machine

stateDiagram-v2
    [*] --> Idle
    Idle --> Fetching : init()
    Fetching --> Valid : tokenReceived
    Fetching --> Error : fetchFailed
    Valid --> Refreshing : refreshRequired()
    Refreshing --> Valid : tokenRefreshed
    Valid --> Error : tokenExpired
    Error --> Fetching : retryFetch()

Component Responsibilities

  • CSRFProtectionProvider:

    • Initializes CSRF protection system
    • Sets up token refresh intervals
    • Manages global error handling for token failures
  • CSRFTokenManager:

    • Fetches CSRF tokens from server
    • Stores tokens in secure storage (HttpOnly cookies or localStorage)
    • Handles token refresh logic
    • Manages token lifecycle and expiration
  • CSRFTokenInput:

    • Renders hidden form field with CSRF token
    • Ensures token is included in form submissions
  • CSRFTokenHeader:

    • Injects CSRF token into HTTP headers for API requests
    • Handles token rotation and refresh
  • ProtectedRoute:

    • Guards protected routes
    • Ensures valid CSRF token exists before navigation
    • Redirects to login if token invalid

Reusability Decisions

The CSRF protection system is designed as a global provider that can be reused across any application requiring CSRF protection. Individual components like CSRFTokenInput and CSRFTokenHeader are highly reusable and can be composed into any form or API request pattern.

Component Lifecycle Considerations

  • Mount: Provider initializes token fetching and refresh intervals
  • Unmount: Clears refresh intervals and cleans up event listeners
  • Token Refresh: Scheduled polling with exponential backoff for failed refreshes
  • Error Handling: Graceful degradation when token fetch fails

State Management Integration

The CSRF protection system integrates with the application's global state management (e.g., Redux or Zustand) to:

  • Store token state in a centralized location
  • Notify other components of token changes
  • Handle authentication state transitions
  • Support optimistic updates for token refresh operations

Performance Considerations

  • Token fetching is debounced to prevent excessive API calls
  • Token refresh is scheduled with exponential backoff
  • Hidden input fields are rendered only when needed
  • HTTP header injection is optimized to avoid unnecessary DOM updates

Security Considerations in Component Design

  • Token storage uses secure HTTP-only cookies when possible
  • Token values are never exposed in DOM or console logs
  • Form fields are properly sanitized and validated
  • Token refresh logic includes proper error handling to prevent token exhaustion

Accessibility Considerations

  • Hidden input fields are properly labeled for screen readers
  • Token injection does not interfere with keyboard navigation
  • Error states provide clear feedback to users without exposing sensitive token information

Testing Strategy

  • Unit tests for token fetching and refresh logic
  • Integration tests for form submission with CSRF tokens
  • E2E tests for protected route access and token expiration handling
  • Mock server responses to test various error conditions

Error Handling in Components

  • Token fetch failures trigger fallback mechanisms
  • Expired tokens automatically attempt refresh
  • Invalid token errors redirect to login or show appropriate error messages
  • Component-level error boundaries prevent cascading failures

3. State Management Design

Local state vs. global state decisions

For CSRF protection, we need to manage:

  • CSRF token storage and lifecycle
  • Token validation status
  • Request queue for failed requests due to token expiration
  • Authentication state that affects token validity

Local state is appropriate for:

  • Per-request token validation status
  • Temporary request queue management

Global state is required for:

  • CSRF token persistence across navigation
  • Token refresh coordination across components
  • Authentication context that affects all API calls

State management approach

Zustand is selected as the state management solution because:

  1. Minimal boilerplate with simple API
  2. Excellent TypeScript support with type inference
  3. No provider wrapping required - direct hook usage
  4. Easy to test and mock
  5. Good performance characteristics with proper memoization

Zustand is preferred over Redux Toolkit for this use case because:

  • Less boilerplate code for simple state management
  • Easier to integrate with existing React patterns
  • Better developer experience for small to medium scale applications

State shape and normalization strategy

interface CsrfState {
  token: string | null;
  isValid: boolean;
  lastRefreshed: number | null;
  pendingRequests: PendingRequest[];
  isRefreshing: boolean;
}

interface PendingRequest {
  id: string;
  url: string;
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
  body?: any;
  timestamp: number;
}

const initialState: CsrfState = {
  token: null,
  isValid: false,
  lastRefreshed: null,
  pendingRequests: [],
  isRefreshing: false,
};

Server state vs. client state separation

Server State (Managed by backend):

  • CSRF token generation and validation
  • Token expiration policies
  • Session management

Client State (Managed by frontend):

  • Current token value and validity status
  • Pending requests queue
  • Token refresh status

Cache invalidation strategy

Token invalidation occurs when:

  1. Token expires (based on server-side TTL)
  2. Server returns 403 Forbidden with CSRF error
  3. User logs out or session ends

When invalidation occurs:

  • Clear current token from state
  • Retry pending requests with new token
  • Invalidate related cached API responses

Optimistic updates design

Optimistic updates are NOT used for CSRF-protected operations because:

  1. CSRF tokens are tied to specific sessions and request contexts
  2. Token validation must be atomic with the actual operation
  3. Incorrect optimistic updates could lead to security vulnerabilities

Instead, we implement:

  • Request queuing for failed requests
  • Automatic token refresh and retry mechanism
  • Proper error handling with user feedback

Critical-path hook implementation

import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import { v4 as uuidv4 } from 'uuid';

interface CsrfState {
  token: string | null;
  isValid: boolean;
  lastRefreshed: number | null;
  pendingRequests: PendingRequest[];
  isRefreshing: boolean;
}

interface CsrfActions {
  setToken: (token: string | null) => void;
  validateToken: () => void;
  addPendingRequest: (request: Omit<PendingRequest, 'id' | 'timestamp'>) => string;
  removePendingRequest: (id: string) => void;
  setRefreshing: (isRefreshing: boolean) => void;
  refreshAndRetryPending: () => Promise<void>;
}

interface PendingRequest {
  id: string;
  url: string;
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
  body?: any;
  timestamp: number;
}

type CsrfStore = CsrfState & CsrfActions;

const useCsrfStore = create<CsrfStore>()(
  immer((set, get) => ({
    token: null,
    isValid: false,
    lastRefreshed: null,
    pendingRequests: [],
    isRefreshing: false,

    setToken: (token) => {
      set((state) => {
        state.token = token;
        state.isValid = !!token;
        if (token) {
          state.lastRefreshed = Date.now();
        }
      });
    },

    validateToken: () => {
      set((state) => {
        state.isValid = !!state.token;
      });
    },

    addPendingRequest: (request) => {
      const id = uuidv4();
      set((state) => {
        state.pendingRequests.push({
          ...request,
          id,
          timestamp: Date.now(),
        });
      });
      return id;
    },

    removePendingRequest: (id) => {
      set((state) => {
        state.pendingRequests = state.pendingRequests.filter(
          (req) => req.id !== id
        );
      });
    },

    setRefreshing: (isRefreshing) => {
      set((state) => {
        state.isRefreshing = isRefreshing;
      });
    },

    refreshAndRetryPending: async () => {
      const { token, isRefreshing, pendingRequests } = get();
      
      if (isRefreshing || !token || pendingRequests.length === 0) {
        return;
      }

      set((state) => {
        state.isRefreshing = true;
      });

      try {
        // Fetch new token from backend
        const newToken = await fetchNewCsrfToken();
        
        set((state) => {
          state.token = newToken;
          state.isValid = true;
          state.lastRefreshed = Date.now();
        });

        // Retry all pending requests with new token
        await retryPendingRequests(pendingRequests, newToken);
      } catch (error) {
        console.error('Failed to refresh CSRF token:', error);
      } finally {
        set((state) => {
          state.isRefreshing = false;
        });
      }
    },
  }))
);

// Custom hook for CSRF-protected API calls
const useCsrfProtectedApi = () => {
  const { token, addPendingRequest, removePendingRequest } = useCsrfStore();
  
  const makeCsrfProtectedRequest = async (
    url: string,
    options?: RequestInit
  ): Promise<Response> => {
    const requestId = addPendingRequest({
      url,
      method: (options?.method || 'GET') as any,
      body: options?.body,
    });

    try {
      const response = await fetch(url, {
        ...options,
        headers: {
          ...options?.headers,
          'X-CSRF-Token': token || '',
        },
      });

      if (response.status === 403) {
        const errorData = await response.json();
        if (errorData?.code === 'CSRF_TOKEN_INVALID') {
          // Token expired, queue retry
          await useCsrfStore.getState().refreshAndRetryPending();
        }
      }

      return response;
    } finally {
      removePendingRequest(requestId);
    }
  };

  return { makeCsrfProtectedRequest };
};

// Helper function to fetch new CSRF token
const fetchNewCsrfToken = async (): Promise<string> => {
  const response = await fetch('/api/csrf-token', {
    method: 'GET',
    credentials: 'include',
  });
  
  if (!response.ok) {
    throw new Error('Failed to fetch CSRF token');
  }
  
  const data = await response.json();
  return data.token;
};

// Helper function to retry pending requests
const retryPendingRequests = async (
  requests: PendingRequest[],
  newToken: string
): Promise<void> => {
  const retryPromises = requests.map(async (req) => {
    try {
      await fetch(req.url, {
        method: req.method,
        body: req.body,
        headers: {
          'X-CSRF-Token': newToken,
        },
      });
    } catch (error) {
      console.error(`Failed to retry request to ${req.url}:`, error);
    }
  });

  await Promise.allSettled(retryPromises);
};

export { useCsrfStore, useCsrfProtectedApi };

The critical-path hook returns:

  • token: Current CSRF token value
  • isValid: Boolean indicating token validity status
  • addPendingRequest: Method to queue requests for retry
  • removePendingRequest: Method to remove completed requests from queue
  • refreshAndRetryPending: Async method to refresh token and retry queued requests

Key state transitions:

  1. Token set → isValid becomes true
  2. Token cleared → isValid becomes false
  3. Refresh in progress → isRefreshing becomes true
  4. Refresh complete → isRefreshing becomes false

Side-effect handlers:

  • Automatic token refresh on 403 errors
  • Request queuing for failed operations
  • Token validation lifecycle management

4. Routing & Navigation

Route Structure and Hierarchy

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

The application follows a protected route structure with:

  • Public routes: Login, Register, Forgot Password (no authentication required)
  • Protected routes: Dashboard, Profile, Settings, Admin (require authentication)
  • Nested route structure for admin features and user settings

Code Splitting Strategy

Route-based code splitting with dynamic imports:

// routes.tsx
const Login = React.lazy(() => import('pages/Login'));
const Dashboard = React.lazy(() => import('pages/Dashboard'));
const Profile = React.lazy(() => import('pages/Profile'));

// Protected route wrapper
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const { isAuthenticated, loading } = useAuth();
  
  if (loading) return <LoadingSpinner />;
  if (!isAuthenticated) return <Navigate to="/login" />;
  
  return children;
};

// Route configuration with lazy loading
const routes = [
  {
    path: '/login',
    element: <Login />,
    lazy: true
  },
  {
    path: '/dashboard',
    element: (
      <ProtectedRoute>
        <Dashboard />
      </ProtectedRoute>
    ),
    lazy: true
  }
];

Justification: Route-based splitting ensures that protected routes are only loaded when needed, reducing initial bundle size. Public pages load immediately for fast first-paint.

// auth-guard.ts
export const AuthGuard: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const { isAuthenticated, loading, refreshToken } = useAuth();
  const navigate = useNavigate();
  
  useEffect(() => {
    if (loading) return;
    
    if (!isAuthenticated) {
      // Store intended destination
      const redirectUrl = window.location.pathname + window.location.search;
      localStorage.setItem('redirectUrl', redirectUrl);
      
      navigate('/login');
    } else {
      // Check token validity and refresh if needed
      refreshToken();
    }
  }, [isAuthenticated, loading, navigate]);
  
  return loading ? <LoadingSpinner /> : children;
};

Authentication flow:

  1. User accesses protected route without valid session
  2. Redirect to login page with intended URL stored in localStorage
  3. After successful login, redirect to stored URL or default dashboard
  4. Token refresh logic handles session expiration gracefully

Deep Linking Support and URL Parameter Design

// URL structure examples:
// /dashboard?tab=analytics&filter=date-range
// /profile/settings?section=notifications&theme=dark
// /admin/users?status=active&page=2

// URL parameter validation and sanitization
const validateAndSanitizeParams = (params: Record<string, string>) => {
  const sanitized = {};
  
  Object.entries(params).forEach(([key, value]) => {
    // Validate parameter names
    if (!/^[a-zA-Z0-9_-]+$/.test(key)) {
      throw new Error(`Invalid parameter name: ${key}`);
    }
    
    // Sanitize values
    sanitized[key] = sanitizeString(value);
  });
  
  return sanitized;
};

Browser History Management and Scroll Restoration

// scroll-restoration.ts
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

export const ScrollToTop: React.FC = () => {
  const location = useLocation();
  
  useEffect(() => {
    // Scroll to top on route change
    window.scrollTo(0, 0);
    
    // Restore scroll position for cached routes (if supported)
    const scrollPosition = sessionStorage.getItem(`scrollPosition-${location.pathname}`);
    if (scrollPosition) {
      window.scrollTo(0, parseInt(scrollPosition));
    }
  }, [location.pathname]);
  
  // Save scroll position before navigation
  useEffect(() => {
    const handleBeforeUnload = () => {
      sessionStorage.setItem(
        `scrollPosition-${location.pathname}`,
        window.scrollY.toString()
      );
    };
    
    window.addEventListener('beforeunload', handleBeforeUnload);
    return () => window.removeEventListener('beforeunload', handleBeforeUnload);
  }, [location.pathname]);
  
  return null;
};

// App.tsx
function App() {
  return (
    <BrowserRouter>
      <ScrollToTop />
      <Routes>
        {/* ... routes */}
      </Routes>
    </BrowserRouter>
  );
}

Scroll restoration strategy:

  • Default to top scroll on route changes
  • Session storage for preserving scroll positions across navigation
  • BeforeUnload event handler to capture current scroll position
  • Optimized for SPA navigation patterns with minimal performance impact

5. API Integration Layer

API Client Design Pattern

The frontend will implement a centralized API client using a fetch wrapper pattern with Axios as the underlying HTTP client. This approach provides:

  • Centralized request/response handling
  • Built-in interceptors for authentication and error handling
  • Type-safe API endpoints with TypeScript integration
  • Consistent error response formatting across all API calls
// api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';

const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000';

const createApiClient = (): AxiosInstance => {
  const client = axios.create({
    baseURL: API_BASE_URL,
    timeout: 10000,
    withCredentials: true, // Required for CSRF cookie handling
  });

  return client;
};

export const apiClient = createApiClient();

Request/Response Interceptors

Request interceptors handle CSRF token injection and authentication headers, while response interceptors manage error responses and token refresh logic.

// api/interceptors.ts
import { apiClient } from './client';
import { getCSRFToken, setCSRFToken } from '@/utils/csrf';

// Request interceptor - inject CSRF token and auth headers
apiClient.interceptors.request.use(
  async (config) => {
    const csrfToken = getCSRFToken();
    if (csrfToken) {
      config.headers['X-CSRF-Token'] = csrfToken;
    }
    
    const authHeader = getAuthHeader();
    if (authHeader) {
      config.headers['Authorization'] = authHeader;
    }
    
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// Response interceptor - handle CSRF token refresh and error responses
apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;
    
    // Handle CSRF token expiration
    if (error.response?.status === 403 && error.response.data?.code === 'CSRF_TOKEN_EXPIRED') {
      // Refresh CSRF token from server
      try {
        const { data } = await apiClient.get('/csrf-token');
        setCSRFToken(data.token);
        
        // Retry original request with new token
        originalRequest.headers['X-CSRF-Token'] = data.token;
        return apiClient(originalRequest);
      } catch (refreshError) {
        // If refresh fails, redirect to login
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }
    
    // Handle authentication errors
    if (error.response?.status === 401) {
      // Clear auth state and redirect to login
      clearAuthState();
      window.location.href = '/login';
    }
    
    return Promise.reject(error);
  }
);

// Helper functions
const getAuthHeader = (): string | null => {
  const token = localStorage.getItem('authToken');
  return token ? `Bearer ${token}` : null;
};

const clearAuthState = () => {
  localStorage.removeItem('authToken');
  sessionStorage.removeItem('authToken');
};

Error Handling and Retry Strategy

Comprehensive error handling includes:

  • HTTP status code categorization (client vs server errors)
  • Retry logic for transient network issues
  • User-friendly error messages with fallbacks
  • Detailed logging for debugging
// api/errorHandler.ts
import { AxiosError } from 'axios';

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

export const handleApiError = (error: AxiosError): ApiError => {
  if (error.response) {
    // Server responded with error status
    const { status, data } = error.response;
    
    return {
      status,
      message: data?.message || `Server Error (${status})`,
      code: data?.code,
      details: data?.details
    };
  } else if (error.request) {
    // Request was made but no response received
    return {
      status: 0,
      message: 'Network Error - Unable to reach server',
      code: 'NETWORK_ERROR'
    };
  } else {
    // Something else happened in setting up the request
    return {
      status: 0,
      message: error.message || 'Unknown Error',
      code: 'UNKNOWN_ERROR'
    };
  }
};

// Retry configuration
export const retryConfig = {
  retries: 3,
  retryDelay: (retryCount: number) => Math.pow(2, retryCount) * 1000,
  retryOn: [500, 502, 503, 504, 408]
};

Loading, Error, and Empty States Design

The API integration layer provides consistent state management for all data operations:

// api/types.ts
export interface ApiResult<T> {
  data: T | null;
  loading: boolean;
  error: ApiError | null;
  refetch: () => void;
}

export interface ApiResponse<T> {
  data: T;
  metadata?: {
    total?: number;
    page?: number;
    pageSize?: number;
  };
}

Data Fetching Strategy

For this feature, we'll use client-side rendering (CSR) with React Query for optimal state management:

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

// 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: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
    },
  },
});

// Generic API query hook
export const useApiQuery = <T>(
  key: string,
  endpoint: string,
  options?: any
) => {
  return useQuery<T>({
    queryKey: [key, endpoint],
    queryFn: async () => {
      const response = await apiClient.get<T>(endpoint);
      return response.data;
    },
    ...options,
  });
};

// Generic API mutation hook
export const useApiMutation = <T, Variables = any>(
  mutationKey: string,
  endpoint: string,
  options?: any
) => {
  return useMutation<T, AxiosError, Variables>({
    mutationKey: [mutationKey, endpoint],
    mutationFn: async (variables) => {
      const response = await apiClient.post<T>(endpoint, variables);
      return response.data;
    },
    ...options,
  });
};

API Response Transformation Layer

All API responses are normalized through a consistent transformation layer:

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

export interface UserResponse {
  id: string;
  name: string;
  email: string;
  role: string;
}

export const transformUserResponse = (response: UserResponse): User => ({
  id: response.id,
  name: response.name,
  email: response.email,
  role: response.role,
});

// 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 AuthServer
    participant ApiServer

    Note over Client,ApiServer: CSRF Token Acquisition
    Client->>AuthServer: GET /csrf-token
    AuthServer-->>Client: CSRF Token Cookie + Header
    
    Note over Client,ApiServer: Protected API Request
    Client->>ApiServer: POST /api/users (with X-CSRF-Token)
    ApiServer->>ApiServer: Validate CSRF Token
    ApiServer-->>Client: 201 Created / 403 Forbidden
    
    Note over Client,ApiServer: Token Refresh
    Client->>AuthServer: GET /csrf-token (expired)
    AuthServer-->>Client: New CSRF Token Cookie + Header
    Client->>ApiServer: Retry POST with new token
    ApiServer-->>Client: 201 Created

CSRF Token Management Strategy

The system implements a robust CSRF token lifecycle management:

  1. Initial Token Acquisition: On login or page load, fetch CSRF token from server
  2. Token Storage: Store in HTTP-only cookie for security and in-memory for client access
  3. Token Validation: Include token in all state-changing requests
  4. Token Refresh: Automatically refresh expired tokens with minimal user interruption
// utils/csrf.ts
export const CSRF_COOKIE_NAME = 'csrf_token';

export const getCSRFToken = (): string | null => {
  // Try to get from cookie first (more secure)
  const cookieValue = document.cookie
    .split('; ')
    .find(row => row.startsWith(`${CSRF_COOKIE_NAME}=`));
  
  if (cookieValue) {
    return cookieValue.split('=')[1];
  }
  
  // Fallback to localStorage for client-side access
  return localStorage.getItem('csrf_token');
};

export const setCSRFToken = (token: string): void => {
  localStorage.setItem('csrf_token', token);
  
  // Set cookie with secure flags (in production)
  if (typeof window !== 'undefined') {
    document.cookie = `${CSRF_COOKIE_NAME}=${token}; path=/; secure; samesite=strict`;
  }
};

export const clearCSRFToken = (): void => {
  localStorage.removeItem('csrf_token');
  document.cookie = `${CSRF_COOKIE_NAME}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
};

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
  • First Contentful Paint (FCP): <1.8s for 95% of users

Bundle Size Budget per Route

  • Landing Page: <200KB (including all dependencies)
  • Dashboard Route: <400KB (with charts, tables, and widgets)
  • Settings Route: <150KB (form components, validation)
  • Profile Route: <100KB (avatar, basic info display)

Critical Rendering Path Optimization

  • HTML: Inline critical CSS and defer non-critical resources
  • CSS: Use CSS-in-JS with selective extraction for critical styles
  • JS: Split bundles by route and lazy-load non-critical components
  • Fonts: Preload critical font subsets with font-display: swap

Image Optimization Strategy

  • Formats: Use WebP for modern browsers, JPEG fallback for older
  • Lazy Loading: Implement loading="lazy" with IntersectionObserver
  • Responsive Images: Use srcSet and sizes attributes for device-specific images
  • Compression: Apply lossless compression for icons, lossy for content images

Font Loading Strategy

  • Preload Critical Fonts: <link rel="preload" as="font"> for body text
  • Font Display: font-display: swap to prevent FOIT
  • Subset Loading: Load only necessary character sets for each locale

Memoization Strategy

  • React.memo: Apply to presentational components with stable props
  • useCallback: Wrap event handlers and function factories in containers
  • useMemo: Cache expensive computations or derived state values (e.g., filtered lists, computed totals)
  • Avoid Over-Memoization: Don't memoize simple props like strings or numbers

Virtualization for Large Lists

  • React Window: Implement virtual scrolling for lists > 100 items
  • Dynamic Item Size: Use FixedSizeList for uniform items, VariableSizeList for variable heights
  • Windowing Strategy: Render only visible items plus a buffer of 5–10 items

Prefetching and Preloading Strategy

  • Route-based Preload: Use preload attribute for critical route dependencies (e.g., dashboard data)
  • Link Prefetching: Implement rel="prefetch" for likely next-page resources (e.g., user profile avatar)
  • Data Prefetching: Use React Query's prefetchQuery for frequently accessed data (e.g., user settings, dashboard metrics)
  • Service Worker: Implement caching strategy for static assets and API responses

Performance Monitoring

  • Lighthouse CI: Integrate automated performance checks in CI pipeline
  • Web Vitals Dashboard: Track real-user performance metrics via Google Analytics 4 or custom RUM
  • Bundle Analysis: Use source-map-explorer and webpack-bundle-analyzer to identify oversized dependencies

Cache Invalidation Strategy

  • ETag-based: Use ETags for API responses to avoid re-fetching unchanged data
  • Cache-Control Headers: Implement max-age and stale-while-revalidate for optimal caching
  • Client-side Cache: Use React Query's cache invalidation patterns to update stale data

Optimistic Updates Design

  • Immediate UI Feedback: Update UI immediately on user action (e.g., form submission)
  • Revert on Error: Rollback optimistic updates when server response fails
  • Loading States: Show loading indicators during optimistic update lifecycle

Critical-Path Hook Implementation

// src/hooks/useOptimisticUpdate.ts
import { useCallback, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';

interface OptimisticUpdateConfig<T> {
  queryKey: string[];
  updateFn: (data: T) => Promise<T>;
  rollback?: (originalData: T) => void;
}

export function useOptimisticUpdate<T>({
  queryKey,
  updateFn,
  rollback
}: OptimisticUpdateConfig<T>) {
  const [optimisticData, setOptimisticData] = useState<T | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const queryClient = useQueryClient();

  const handleUpdate = useCallback(
    async (newData: T) => {
      setIsLoading(true);
      setOptimisticData(newData);

      try {
        const result = await updateFn(newData);
        queryClient.setQueryData(queryKey, result);
        setOptimisticData(null);
      } catch (error) {
        rollback?.(optimisticData as T);
        setOptimisticData(null);
        throw error;
      } finally {
        setIsLoading(false);
      }
    },
    [updateFn, queryKey, rollback, optimisticData]
  );

  return {
    handleUpdate,
    isLoading,
    optimisticData
  };
}

Performance Budget Implementation

// webpack.config.js - Performance budget configuration
module.exports = {
  performance: {
    maxAssetSize: 250000,
    maxEntrypointSize: 300000,
    hints: 'error'
  }
};

Bundle Analysis Strategy

  • Build-time Analysis: Run webpack-bundle-analyzer after each build to identify large dependencies
  • Code Splitting: Implement route-based code splitting using React.lazy and Suspense
  • Tree Shaking: Ensure all libraries support ES modules for dead code elimination

Memory Management

  • Cleanup Side Effects: Implement proper cleanup in useEffect hooks to prevent memory leaks
  • Event Listener Management: Use useCallback and proper cleanup in event handlers
  • Large Data Handling: Implement pagination or virtualization for large datasets to prevent memory bloat

Resource Loading Prioritization

  • Critical Path Resources: Load essential JS/CSS in <head> for fastest render
  • Non-Critical Resources: Defer loading of analytics, third-party widgets, and non-essential features
  • Resource Hints: Use rel="preload" for critical resources, rel="prefetch" for likely future resources

Accessibility Performance Considerations

  • Keyboard Navigation: Ensure all performance optimizations don't break keyboard-only users
  • Screen Reader Compatibility: Verify that virtualized lists and lazy-loaded content work with screen readers
  • Focus Management: Maintain proper focus order even when components are dynamically loaded or replaced

Monitoring and Alerting

  • Real User Monitoring: Track performance metrics in production with error boundaries and logging
  • Threshold-based Alerts: Configure alerts for LCP > 3s, CLS > 0.25, INP > 500ms
  • Performance Regression Detection: Use automated tools to detect performance regressions in CI/CD pipeline

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:

  • Application-level error boundary - Covers entire app to catch unexpected runtime errors
  • Feature-specific boundaries - Wrap major feature components (e.g., dashboard, forms)
  • Component-level boundaries - For critical sub-components that might fail independently
graph TD
    A[App] --> B[ErrorBoundary]
    B --> C[Dashboard]
    B --> D[UserForm]
    B --> E[Navigation]
    C --> F[DashboardGrid]
    C --> G[AnalyticsChart]
    D --> H[FormInputs]
    D --> I[SubmitButton]

Global Error Handler Design

A centralized error handler manages uncaught exceptions and provides consistent feedback:

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

interface GlobalErrorState {
  hasError: boolean;
  error?: Error;
  errorInfo?: React.ErrorInfo;
}

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

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

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // Log to monitoring service
    console.error('Global Error:', error, errorInfo);
    
    // Send to error tracking service (e.g., Sentry)
    if (window.Sentry) {
      window.Sentry.captureException(error, {
        contexts: { react: errorInfo }
      });
    }
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-container">
          <h1>Something went wrong.</h1>
          <p>We're working to fix this issue.</p>
          <button onClick={() => window.location.reload()}>
            Reload Page
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

export default GlobalErrorHandler;

Offline / Network Failure Handling

Offline resilience is achieved through:

  1. Network status monitoring using navigator.onLine API
  2. Retry mechanisms with exponential backoff for transient failures
  3. Offline queue for operations that can be retried later
// src/core/network/NetworkManager.ts
class NetworkManager {
  private isOnline: boolean = navigator.onLine;
  private offlineQueue: QueueItem[] = [];
  
  constructor() {
    window.addEventListener('online', this.handleOnline);
    window.addEventListener('offline', this.handleOffline);
  }

  private handleOnline = () => {
    this.isOnline = true;
    this.processOfflineQueue();
  };

  private handleOffline = () => {
    this.isOnline = false;
  };

  async withRetry<T>(
    operation: () => Promise<T>,
    maxRetries = 3,
    baseDelay = 1000
  ): Promise<T> {
    let lastError: Error;
    
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        return await operation();
      } catch (error) {
        lastError = error as Error;
        
        if (attempt === maxRetries || !this.isOnline) {
          throw lastError;
        }
        
        // Exponential backoff with jitter
        const delay = baseDelay * Math.pow(2, attempt) + 
                     Math.random() * 1000;
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
    
    throw lastError;
  }

  queueOperation(operation: QueueItem) {
    if (!this.isOnline) {
      this.offlineQueue.push(operation);
      return true;
    }
    return false;
  }

  private async processOfflineQueue() {
    while (this.offlineQueue.length > 0) {
      const operation = this.offlineQueue.shift();
      try {
        await operation.execute();
      } catch (error) {
        // Re-queue failed operations
        this.offlineQueue.push(operation);
      }
    }
  }
}

export const networkManager = new NetworkManager();

Graceful Degradation Patterns

When features fail, provide fallback experiences:

// src/components/AnalyticsChart.tsx
interface ChartProps {
  data?: ChartData[];
  loading?: boolean;
  error?: Error | null;
}

const AnalyticsChart: React.FC<ChartProps> = ({ 
  data, 
  loading, 
  error 
}) => {
  if (loading) {
    return <div className="skeleton-chart">Loading chart...</div>;
  }

  if (error) {
    return (
      <div className="chart-fallback">
        <p>Chart unavailable due to: {error.message}</p>
        <button onClick={() => window.location.reload()}>
          Retry
        </button>
      </div>
    );
  }

  if (!data || data.length === 0) {
    return (
      <div className="chart-empty">
        <p>No data available</p>
      </div>
    );
  }

  return (
    <div className="chart-container">
      {/* Chart rendering logic */}
    </div>
  );
};

User Feedback on Errors

Consistent error communication patterns:

  1. Toast notifications for non-blocking errors
  2. Inline error messages for form validation
  3. Fallback UIs for critical component failures
// src/core/toast/ToastManager.tsx
interface Toast {
  id: string;
  message: string;
  type: 'success' | 'error' | 'warning' | 'info';
  duration?: number;
}

class ToastManager {
  private toasts: Toast[] = [];
  private listeners: Array<(toasts: Toast[]) => void> = [];

  addToast(message: string, type: Toast['type'] = 'info', duration = 5000) {
    const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
    
    const toast: Toast = {
      id,
      message,
      type,
      duration
    };

    this.toasts.push(toast);
    this.notifyListeners();
    
    // Auto-remove toast
    if (duration > 0) {
      setTimeout(() => this.removeToast(id), duration);
    }
  }

  removeToast(id: string) {
    this.toasts = this.toasts.filter(toast => toast.id !== id);
    this.notifyListeners();
  }

  private notifyListeners() {
    this.listeners.forEach(listener => listener(this.toasts));
  }

  subscribe(listener: (toasts: Toast[]) => void) {
    this.listeners.push(listener);
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener);
    };
  }
}

export const toastManager = new ToastManager();

Error Recovery Strategies

Implement structured recovery paths:

  1. Automatic retry for transient network issues
  2. User-initiated refresh for recoverable errors
  3. Offline mode for persistent failures
  4. Error logging with context for debugging
// src/core/error/ErrorRecovery.ts
class ErrorRecovery {
  static async handleApiError(error: any, context: string) {
    const errorInfo = {
      timestamp: new Date().toISOString(),
      context,
      userAgent: navigator.userAgent,
      url: window.location.href,
      error: error.message || String(error)
    };

    // Log to monitoring service
    if (window.Sentry) {
      window.Sentry.captureException(error, {
        contexts: { errorInfo }
      });
    }

    // Handle specific error types
    if (error.response?.status === 401) {
      // Redirect to login for authentication errors
      window.location.href = '/login';
      return;
    }

    if (error.response?.status === 403) {
      // Show permission error
      toastManager.addToast(
        'You do not have permission to perform this action',
        'error'
      );
      return;
    }

    // Show generic error for other cases
    toastManager.addToast(
      'An unexpected error occurred. Please try again.',
      'error'
    );
  }
}

8. Testing Strategy

Unit Testing

  • Component tests: 90%+ coverage for all presentational and container components using React Testing Library
  • Custom hook tests: 100% coverage for all custom hooks including error states, loading states, and side-effect execution
  • Utility function tests: 100% coverage for all security-related utilities (token generation, validation, sanitization)
  • State management tests: Test reducer logic and async action handling in isolation
  • Target coverage: 85%+ line coverage, with focus on security boundary conditions and error paths

Integration Testing Strategy

  • API integration tests: Validate token flow between frontend and backend services (CSRF token exchange, validation)
  • Form submission integration: End-to-end flow testing for forms that require CSRF tokens
  • Authentication boundary tests: Verify token invalidation on session changes and logout flows
  • Cross-component interaction: Test token propagation between parent-child components and context providers

E2E Testing Scope and Tool

  • Tool: Playwright (chosen for better cross-browser support and network interception capabilities)
  • Covered flows:
    • User login → CSRF token generation → form submission
    • Token expiration handling and refresh flow
    • Session hijacking prevention (token invalidation on session changes)
    • Multi-tab browser behavior and token synchronization
  • Network interception: Test token injection in request headers and validation in response handling

Visual Regression Testing

  • Tool: Chromatic (with Playwright integration for automated screenshot capture)
  • Test cases:
    • Token mismatch error UI rendering
    • Session timeout error states
    • Form submission failure states with proper error messages
  • Viewport coverage: Desktop, tablet, mobile breakpoints

Accessibility Testing Automation

  • Tool: axe-core with React Testing Library integration
  • Tests:
    • Token error messages must have proper ARIA labels and keyboard focus
    • Form elements must maintain accessible names when CSRF tokens are injected
    • Error states must be announced by screen readers
  • Coverage: 100% of interactive components with accessibility testing

Test Data Strategy and Fixtures

  • Security test fixtures:
    • Valid CSRF tokens with different expiration times (immediate, 5min, 1hr)
    • Invalid token formats (malformed, expired, mismatched)
    • Token tampering scenarios (man-in-the-middle attack simulations)
  • Test data isolation: Use unique token values per test run to prevent cross-test contamination
  • Mock server setup: Use MSW (Mock Service Worker) for realistic API response simulation with token validation logic
  • Environment-specific fixtures: Separate test data for local dev, staging, and production-like environments

9. Accessibility (A11y)

WCAG 2.1 AA compliance checklist for this feature

The CSRF protection implementation must maintain full accessibility compliance:

  • Keyboard Navigation: All form elements and actions must be fully operable via keyboard (Tab, Enter, Space)
  • Focus Management: Proper focus indicators and trapping for modals/dialogs
  • Screen Reader Support: ARIA labels and roles for dynamic content updates
  • Contrast Ratios: Text must meet 4.5:1 contrast ratio for normal text and 3:1 for large text
  • Error Handling: Clear error messages with proper ARIA states (aria-invalid, aria-describedby)
  • Form Labels: All form fields must have accessible labels associated with them
  • Skip Links: Keyboard-accessible skip links to bypass repetitive navigation

Keyboard navigation support

All CSRF-protected forms and actions must support complete keyboard navigation:

// Keyboard navigation handler for CSRF-protected forms
const handleKeyboardNavigation = (event: React.KeyboardEvent) => {
  if (event.key === 'Enter' && event.target instanceof HTMLButtonElement) {
    event.preventDefault();
    handleSubmitWithCSRF(event);
  }
  
  if (event.key === 'Tab') {
    // Handle focus trapping in modals
    handleFocusTrap(event);
  }
};

Focus management strategy

Proper focus handling for modals and dynamic content:

// Focus trap implementation for CSRF confirmation dialogs
const useFocusTrap = (isOpen: boolean, modalRef: React.RefObject<HTMLDivElement>) => {
  useEffect(() => {
    if (!isOpen) return;

    const focusableElements = modalRef.current?.querySelectorAll(
      'button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
    ) || [];

    const firstElement = focusableElements[0] as HTMLElement;
    const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;

    const handleFocus = (event: FocusEvent) => {
      if (!modalRef.current?.contains(event.target as Node)) {
        lastElement.focus();
      }
    };

    firstElement?.focus();

    return () => {
      // Restore focus to previous element
      document.activeElement?.blur();
    };
  }, [isOpen]);
};

ARIA roles and attributes plan

// CSRF error state with proper ARIA attributes
const CSRFErrorDisplay = ({ message }: { message: string }) => (
  <div 
    role="alert"
    aria-live="assertive"
    aria-atomic="true"
    className="csrf-error-container"
  >
    <span 
      role="status" 
      aria-label={`CSRF protection error: ${message}`}
      className="csrf-error-message"
    >
      {message}
    </span>
  </div>
);

// CSRF token input with proper labeling
const CSRFTokenInput = ({ token }: { token: string }) => (
  <input
    type="hidden"
    name="csrf_token"
    value={token}
    aria-label="CSRF protection token"
    aria-describedby="csrf-token-hint"
  />
);

Screen reader compatibility

All dynamic CSRF state changes must be announced to screen readers:

// Screen reader announcement for CSRF events
const ScreenReaderAnnouncer = ({ message }: { message: string }) => (
  <div 
    aria-live="polite" 
    aria-atomic="true"
    className="sr-only"
  >
    {message}
  </div>
);

// Example usage in CSRF validation flow
const announceCSRFStatus = (status: 'valid' | 'invalid' | 'expired') => {
  const messages = {
    valid: 'CSRF validation successful',
    invalid: 'CSRF validation failed, please refresh the page',
    expired: 'CSRF token has expired, please refresh the page'
  };
  
  return <ScreenReaderAnnouncer message={messages[status]} />;
};

Focus restoration patterns

// Focus restoration for CSRF-protected actions
const useCSRFFocusRestore = (action: string) => {
  const previousActiveElement = useRef<HTMLElement | null>(null);
  
  useEffect(() => {
    // Store current focus before CSRF action
    previousActiveElement.current = document.activeElement as HTMLElement;
    
    return () => {
      // Restore focus after action completes
      if (previousActiveElement.current) {
        previousActiveElement.current.focus();
      }
    };
  }, [action]);
};

Accessibility testing automation

All accessibility checks must be automated:

// Accessibility test assertions for CSRF forms
describe('CSRF Form Accessibility', () => {
  it('should have proper focus management', async () => {
    await render(<CSRFProtectedForm />);
    
    // Verify all form elements are keyboard accessible
    const inputs = await screen.getAllByRole('textbox');
    expect(inputs).toHaveLength(2);
    
    // Verify proper ARIA attributes
    const tokenInput = await screen.getByRole('textbox', { name: 'CSRF token' });
    expect(tokenInput).toHaveAttribute('aria-label');
  });
  
  it('should announce CSRF errors to screen readers', async () => {
    const { container } = render(<CSRFErrorDisplay message="Invalid token" />);
    
    const srAnnouncer = container.querySelector('[aria-live="polite"]');
    expect(srAnnouncer).toBeInTheDocument();
    
    const errorText = await screen.findByText('Invalid token');
    expect(errorText).toHaveAttribute('aria-live', 'assertive');
  });
});

Keyboard navigation testing

// Test case for keyboard navigation in CSRF-protected forms
describe('CSRF Form Keyboard Navigation', () => {
  it('should allow tab navigation through form fields', async () => {
    const { container } = render(<CSRFProtectedForm />);
    
    const firstInput = await screen.getByLabelText('Username');
    const secondInput = await screen.getByLabelText('Password');
    
    // Simulate tab navigation
    firstInput.focus();
    await userEvent.tab();
    
    expect(secondInput).toHaveFocus();
  });
  
  it('should submit form with Enter key', async () => {
    const { container } = render(<CSRFProtectedForm />);
    
    const submitButton = await screen.getByRole('button', { name: 'Submit' });
    
    // Simulate Enter key press
    await userEvent.type(submitButton, '{enter}');
    
    // Verify form submission event was triggered
    expect(mockSubmitHandler).toHaveBeenCalled();
  });
});

10. Security Considerations

XSS Prevention Strategy

Content Sanitization: All user-generated content must be sanitized before rendering to prevent script injection. Implement a robust HTML sanitizer like DOMPurify that strips dangerous tags and attributes while preserving safe formatting.

// Example implementation using DOMPurify
import DOMPurify from 'dompurify';

const sanitizeHTML = (rawHTML: string): string => {
  return DOMPurify.sanitize(rawHTML, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
    ALLOWED_ATTR: ['class'],
  });
};

Input Validation: All form inputs must be validated on both client and server side with strict regex patterns and length constraints. Never trust user input.

Template Literal Escaping: When dynamically generating HTML content, ensure proper escaping of template literals to prevent injection attacks.

CSRF Protection

Token Generation & Management: Implement a robust CSRF token system that:

  • Generates unique tokens per user session
  • Regenerates tokens on sensitive operations (form submissions, API calls)
  • Stores tokens in HTTP-only cookies for maximum security
  • Includes tokens in all state-changing requests (POST, PUT, DELETE)
// CSRF token management hook
import { useEffect, useState } from 'react';
import { getCSRFToken } from '../api/csrf';

export const useCSRFProtection = () => {
  const [csrfToken, setCsrfToken] = useState<string | null>(null);
  
  useEffect(() => {
    const fetchCSRFToken = async () => {
      try {
        const token = await getCSRFToken();
        setCsrfToken(token);
      } catch (error) {
        console.error('Failed to fetch CSRF token:', error);
      }
    };
    
    fetchCSRFToken();
  }, []);

  const getCSRFHeaders = () => ({
    'X-CSRF-Token': csrfToken || '',
    'X-Requested-With': 'XMLHttpRequest'
  });

  return { csrfToken, getCSRFHeaders };
};

Double Submit Cookie Pattern: Implement double-submit cookie pattern where:

  • Server sets a CSRF cookie with a random token value
  • Client includes the same token in request headers or form fields
  • Server validates that both values match before processing

Content Security Policy (CSP) Headers

Header Implementation: Configure strict CSP headers to prevent XSS and other injection attacks:

// CSP header configuration in server middleware
const cspHeaders = {
  'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';",
  'X-Content-Type-Options': 'nosniff',
  'X-Frame-Options': 'DENY',
  'X-XSS-Protection': '1; mode=block'
};

CSP Policy Components:

  • default-src 'self' - Only allow resources from same origin
  • script-src - Allow inline scripts only for critical functionality, block eval
  • style-src - Block unsafe inline styles to prevent CSS injection
  • img-src - Restrict image sources to prevent tracking

Sensitive Data Handling in Frontend State

Secure Storage: Never store sensitive data like authentication tokens or session IDs in:

  • LocalStorage (vulnerable to XSS)
  • SessionStorage (vulnerable to XSS)
  • React state without proper sanitization

Secure Token Storage: Use HTTP-only cookies for session tokens and ensure they're:

  • Secure (HTTPS only)
  • HttpOnly (prevents JavaScript access)
  • SameSite (prevents CSRF attacks)
// Secure token handling in React context
interface AuthContextType {
  accessToken: string | null;
  refreshToken: string | null;
  setTokens: (access: string, refresh: string) => void;
  clearTokens: () => void;
}

const AuthContext = createContext<AuthContextType>({
  accessToken: null,
  refreshToken: null,
  setTokens: () => {},
  clearTokens: () => {}
});

export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
  const [accessToken, setAccessToken] = useState<string | null>(null);
  const [refreshToken, setRefreshToken] = useState<string | null>(null);

  // Token setting logic that ensures secure storage
  const setTokens = (access: string, refresh: string) => {
    // Store in secure HTTP-only cookie instead of React state
    // This is a simplified example - actual implementation would use server-side storage
    setAccessToken(access);
    setRefreshToken(refresh);
  };

  const clearTokens = () => {
    setAccessToken(null);
    setRefreshToken(null);
  };

  return (
    <AuthContext.Provider value={{ accessToken, refreshToken, setTokens, clearTokens }}>
      {children}
    </AuthContext.Provider>
  );
};

Input Sanitization Approach

Form Validation: Implement comprehensive input validation:

  • Client-side: Real-time validation with clear error messages
  • Server-side: Complete re-validation before processing
  • Rate limiting on form submissions to prevent abuse

API Request Sanitization: All API requests must:

  • Validate request method and headers
  • Sanitize all URL parameters and query strings
  • Implement proper content-type checking
  • Validate request payload structure

Security Headers Implementation

Header Configuration: Implement security headers at the server level:

// Express middleware for security headers
const securityHeaders = (req: Request, res: Response, next: NextFunction) => {
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('X-XSS-Protection', '1; mode=block');
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  next();
};

Security Headers Benefits:

  • Strict-Transport-Security: Forces HTTPS connections
  • X-Content-Type-Options: Prevents MIME type sniffing
  • X-Frame-Options: Protects against clickjacking attacks
  • Referrer-Policy: Controls referrer information sent to external sites

Security Testing Considerations

Vulnerability Scanning: Integrate automated security scanning tools:

  • SAST (Static Application Security Testing) for code analysis
  • DAST (Dynamic Application Security Testing) for runtime vulnerability detection
  • Dependency scanning for known vulnerabilities in third-party packages

Security Review Process: Establish a security review process that includes:

  • Code reviews with security focus
  • Penetration testing of critical endpoints
  • Regular security audits of authentication flows

This comprehensive security approach ensures that the frontend application is resilient against common web vulnerabilities while maintaining a secure user experience. The design prioritizes defense in depth, implementing multiple layers of protection to safeguard against both client-side and server-side threats.

11. Internationalization (i18n)

Translation Key Structure and Naming Convention

Following BEM-style naming with hierarchical organization:

// Translation keys organized by feature and context
{
  "common": {
    "buttons": {
      "save": "Save",
      "cancel": "Cancel",
      "submit": "Submit"
    },
    "labels": {
      "username": "Username",
      "email": "Email"
    }
  },
  "auth": {
    "login": {
      "title": "Login to Your Account",
      "form": {
        "username_label": "Username or Email",
        "password_label": "Password",
        "remember_me": "Remember me"
      },
      "errors": {
        "invalid_credentials": "Invalid username or password",
        "account_locked": "Account locked due to too many failed attempts"
      }
    }
  },
  "dashboard": {
    "welcome_message": "Welcome back, {{name}}!",
    "stats": {
      "total_users": "Total Users: {{count}}",
      "revenue": "Revenue: {{amount}}"
    }
  }
}

RTL Support Considerations

RTL layout handling through CSS classes and direction properties:

/* CSS for RTL support */
[dir="rtl"] .text-left {
  text-align: right;
}

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

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

Date, Number, Currency Formatting Strategy

Using Intl API with fallbacks:

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

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

export const formatNumber = (num: number, locale: string, options?: Intl.NumberFormatOptions) => {
  return new Intl.NumberFormat(locale, {
    maximumFractionDigits: 2,
    ...options
  }).format(num);
};

Locale Detection and Switching Strategy

Automatic detection with manual override:

// i18n context implementation
interface I18nContextType {
  locale: string;
  setLocale: (locale: string) => void;
  t: (key: string, params?: Record<string, unknown>) => 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: () => ''
});

// Automatic locale detection
const detectLocale = (): string => {
  const stored = localStorage.getItem('preferred-locale');
  if (stored) return stored;
  
  const browserLang = navigator.language || 'en-US';
  const supported = ['en-US', 'fr-FR', 'es-ES', 'ar-SA'];
  return supported.includes(browserLang) ? browserLang : 'en-US';
};

// Locale switching with persistence
const useI18n = () => {
  const [locale, setLocale] = useState<string>(detectLocale());
  
  const changeLocale = (newLocale: string) => {
    setLocale(newLocale);
    localStorage.setItem('preferred-locale', newLocale);
    document.documentElement.lang = newLocale;
    document.documentElement.dir = newLocale.startsWith('ar') ? 'rtl' : 'ltr';
  };
  
  return { locale, setLocale: changeLocale };
};

Translation File Organization

Separate files per language with hierarchical structure:

src/
  locales/
    en-US.json
    fr-FR.json
    es-ES.json
    ar-SA.json
  i18n/
    index.ts
    utils.ts
    types.ts

Translation Loading Strategy

Lazy loading with dynamic imports:

// Dynamic translation loader
const loadTranslations = async (locale: string) => {
  try {
    const translations = await import(`../locales/${locale}.json`);
    return translations.default;
  } catch (error) {
    console.error(`Failed to load translations for ${locale}`, error);
    return {};
  }
};

// Translation provider with loading state
const TranslationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [translations, setTranslations] = useState<Record<string, string>>({});
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const load = async () => {
      const loaded = await loadTranslations(detectLocale());
      setTranslations(loaded);
      setLoading(false);
    };
    
    load();
  }, []);
  
  const t = (key: string, params?: Record<string, unknown>): string => {
    let translation = translations[key] || key;
    
    if (params) {
      Object.entries(params).forEach(([param, value]) => {
        translation = translation.replace(`{{${param}}}`, String(value));
      });
    }
    
    return translation;
  };
  
  return (
    <I18nContext.Provider value={{ locale, t, setLocale, formatDate, formatCurrency }}>
      {loading ? <div>Loading translations...</div> : children}
    </I18nContext.Provider>
  );
};

Accessibility Considerations

Ensuring proper language attributes and screen reader support:

// Language attribute handling for accessibility
const I18nApp = () => {
  const { locale } = useI18n();
  
  return (
    <div 
      lang={locale} 
      dir={locale.startsWith('ar') ? 'rtl' : 'ltr'}
      className="i18n-app"
    >
      <App />
    </div>
  );
};

// Screen reader friendly date formatting
const FormattedDate: React.FC<{
  date: Date;
  locale: string;
  options?: Intl.DateTimeFormatOptions;
}> = ({ date, locale, options }) => {
  const formatted = new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    ...options
  }).format(date);
  
  return (
    <span 
      aria-label={`Date: ${formatted}`}
      role="text"
    >
      {formatted}
    </span>
  );
};

Testing Strategy

Comprehensive i18n testing with multiple locales:

// Test cases for i18n functionality
describe('I18n', () => {
  test('loads translations for supported locales', async () => {
    const enTranslations = await loadTranslations('en-US');
    expect(enTranslations).toBeDefined();
  });
  
  test('handles RTL layout correctly', () => {
    const { container } = render(
      <div dir="rtl">
        <I18nProvider locale="ar-SA">
          <TestComponent />
        </I18nProvider>
      </div>
    );
    
    expect(container).toHaveAttribute('dir', 'rtl');
  });
  
  test('formats currency correctly in different locales', () => {
    const result = formatCurrency(1234.56, 'en-US', 'USD');
    expect(result).toBe('$1,234.56');
    
    const resultFr = formatCurrency(1234.56, 'fr-FR', 'EUR');
    expect(resultFr).toBe('1 234,56 €');
  });
});

12. Monitoring & Observability

Real User Monitoring (RUM) metrics to track

The frontend should capture key RUM metrics to understand user experience and performance:

  • First Contentful Paint (FCP): Time from page load to first content rendering
  • Largest Contentful Paint (LCP): Time to render the largest image/text element
  • First Input Delay (FID): Time from first user interaction to browser response
  • Cumulative Layout Shift (CLS): Visual stability of content during load
  • Time to Interactive (TTI): When page becomes fully interactive
  • Page Load Time: Complete page load duration

These metrics should be collected via the Web Vitals API and sent to monitoring systems with user session context.

Error tracking integration

Integrate with error monitoring tools like Sentry or Datadog to capture:

  • Uncaught JavaScript errors: Caught by global error boundaries
  • Unhandled promise rejections: Using window.addEventListener('unhandledrejection')
  • Network errors: API request failures with status codes
  • Console errors: Runtime warnings and errors from console.log

Error reports should include:

  • Stack trace with source maps
  • User session context (user ID, session ID)
  • Browser and device information
  • URL path and query parameters
  • Request/response payloads where safe

Custom events and user journey tracking

Implement custom event tracking for key user actions:

// Example custom event tracking system
export interface UserEvent {
  name: string;
  timestamp: number;
  userId?: string;
  sessionId: string;
  properties?: Record<string, any>;
}

export const trackUserEvent = (event: UserEvent) => {
  // Send to analytics backend or real-time monitoring service
  const payload = {
    ...event,
    timestamp: Date.now(),
    userAgent: navigator.userAgent,
    viewport: `${window.innerWidth}x${window.innerHeight}`
  };
  
  // Example: send to custom analytics endpoint
  fetch('/api/analytics/events', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload)
  });
};

Performance monitoring and regression detection

Implement performance baselines and alerting:

  • Baseline tracking: Store historical performance metrics per route
  • Regression detection: Alert when performance degrades beyond threshold (e.g., 10% increase in LCP)
  • Route-specific metrics: Track performance per page or feature
  • Threshold-based alerts: Configure thresholds for key metrics (LCP > 2.5s, FID > 100ms)

Implementation approach

// Performance monitoring hook - capture key metrics and send to observability backend
import { useEffect, useRef } from 'react';

export const usePerformanceMonitoring = () => {
  const observerRef = useRef<PerformanceObserver | null>(null);

  useEffect(() => {
    // Setup Web Vitals tracking
    if ('PerformanceObserver' in window && 'getEntriesByType' in performance) {
      observerRef.current = new PerformanceObserver((list) => {
        list.getEntries().forEach((entry) => {
          if (entry.entryType === 'largest-contentful-paint') {
            // Send LCP metric to monitoring service
            sendMetric('lcp', entry.startTime, {
              value: entry.value,
              element: entry.element?.tagName || 'unknown'
            });
          }
        });
      });

      observerRef.current.observe({ entryTypes: ['largest-contentful-paint'] });
    }

    return () => {
      if (observerRef.current) {
        observerRef.current.disconnect();
      }
    };
  }, []);

  const sendMetric = (metricName: string, value: number, properties?: any) => {
    // Send to monitoring backend or analytics service
    console.log(`[RUM] ${metricName}:`, value, properties);
  };
};

Integration with backend monitoring

Backend services should correlate frontend metrics with server-side performance:

  • Request correlation: Include request IDs in both frontend and backend logs
  • Session tracking: Match user sessions across frontend and backend services
  • Error context: Include frontend error context in backend error reports

Alerting strategy

Configure alerting for:

  • High error rates: >5% of requests failing
  • Performance regressions: >10% degradation in key metrics
  • Critical user flows: Failures in authentication, checkout, or content loading

Alerts should be sent to:

  • Slack/Teams channels for immediate visibility
  • Email notifications for critical issues
  • Incident management systems for tracking and resolution

Data privacy considerations

All monitoring data should:

  • Exclude sensitive user information (passwords, PII)
  • Comply with privacy regulations (GDPR, CCPA)
  • Allow user opt-out mechanisms where required
  • Be aggregated and anonymized where possible

Dashboard and visualization

Create dashboards to visualize:

  • Real-time performance metrics
  • Error rate trends over time
  • User journey completion rates
  • Geographic and device-based performance differences

These dashboards should be accessible to engineering teams for proactive issue detection and resolution.

13. File & Folder Structure (brief — implementation detail, not primary LLD interview content)

Proposed Directory Structure

/src
  /features
    /auth
      /components
      /hooks
      /services
      /types
    /dashboard
      /components
      /hooks
      /services
      /types
  /shared
    /components
    /hooks
    /utils
    /types
    /constants
  /core
    /api
    /auth
    /error
    /storage
  /routes
  /styles
  /types

Module Boundaries and Import Rules

  • Feature modules are self-contained with internal imports only
  • Shared components/utilities can be imported by any feature module
  • Core modules (auth, api) are top-level and can be imported by features but not vice versa
  • No cross-feature imports to maintain module isolation

Shared vs. Feature-Specific Code Separation

  • Shared: Reusable UI components, utility functions, type definitions, constants
  • Feature-specific: Components, services, hooks, and types that are only relevant to a specific feature area
  • Core: Infrastructure-level code (auth guards, API clients) that all features depend on

This structure enables clear separation of concerns while maintaining flexibility for feature development and code reuse. The feature-based organization allows teams to own their modules independently while maintaining consistent patterns across the codebase.

14. Dependencies & Third-Party Libraries

Key Libraries Selected with Justification

1. Axios - Primary HTTP client for API communication

  • Justification: Battle-tested, robust error handling, request/response interceptors, and extensive ecosystem support. Provides built-in CSRF token handling through interceptors.
  • Bundle size: ~12KB gzipped
  • Security: Built-in CORS support, request/response transformation capabilities

2. React Hook Form - Form state management and validation

  • Justification: Optimized for performance, excellent TypeScript support, built-in validation with Zod integration, and minimal re-renders through field-level control.
  • Bundle size: ~15KB gzipped
  • Security: Prevents XSS through proper input sanitization and validation

3. React Query (TanStack Query) - Server state management

  • Justification: Excellent caching strategy, automatic refetching, optimistic updates, and built-in error recovery. Provides seamless integration with React's concurrent rendering.
  • Bundle size: ~18KB gzipped
  • Security: Handles sensitive data in cache with proper cleanup and memory management

4. React Router v6 - Client-side routing

  • Justification: Clean API, nested routing support, built-in lazy loading, and excellent integration with React Query for data fetching.
  • Bundle size: ~10KB gzipped
  • Security: Built-in route protection and authentication guards

5. Styled Components / Tailwind CSS - Styling solution

  • Justification: Component-scoped styling prevents CSS pollution, supports theming, and enables better developer experience with type safety.
  • Bundle size: ~20KB (Tailwind) / ~15KB (Styled Components)
  • Security: No runtime security concerns, CSS isolation prevents XSS via CSS injection

Alternatives Considered and Rejected

1. Fetch API + Custom Wrapper

  • Rejected because: Requires significant boilerplate for error handling, caching, and interceptors. Would need to build CSRF protection from scratch.

2. Redux Toolkit + Redux-Saga

  • Rejected because: Overkill for this feature's scope. React Query provides better server state management with less boilerplate and better performance characteristics.

3. Formik

  • Rejected because: Less performant than React Hook Form, particularly with large forms. More verbose API and less TypeScript support.

4. Emotion

  • Rejected because: While powerful, it introduces more complexity than Tailwind for this use case and requires additional configuration for security.

5. React-Table / react-virtual

  • Rejected because: Not relevant to CSRF protection feature, but would be considered for list rendering if needed.

Licensing and Bundle Size Impact Assessment

Licensing Compliance:

  • All selected libraries are MIT or BSD-3-Clause licensed, suitable for enterprise use
  • No GPL dependencies that would require open-sourcing our codebase
  • Regular security scanning integration in CI pipeline to detect license violations

Bundle Size Budget:

  • Target total vendor bundle size: ~80KB (gzipped) for core features
  • Current selection accounts for ~55KB of that total, leaving room for future feature additions
  • Tree-shaking and code splitting strategies will reduce actual runtime bundle size

Performance Impact:

  • All libraries are optimized for production builds with proper minification
  • React Query's automatic garbage collection prevents memory leaks
  • Axios interceptors are designed for minimal performance overhead

Security Considerations:

  • All libraries undergo security audits before inclusion
  • Regular dependency updates with security patching strategy (monthly security scans)
  • No known vulnerabilities in current library versions within our usage scope

15. Open Questions, Risks & Trade-offs

Decisions that need product/engineering alignment

  • CSRF token storage location: Should tokens be stored in HTTP-only cookies, localStorage, or sessionStorage? This decision directly impacts the security model and cross-origin support requirements.
  • Token refresh strategy: How often should CSRF tokens be rotated? Should they be tied to session lifecycle or have shorter TTLs?
  • Cross-origin support: Will the application need to support cross-origin requests? If so, how should CSRF protection be handled for these scenarios?
  • Authentication method alignment: How does CSRF protection integrate with the existing authentication system (OAuth, JWT, session-based)?

Known trade-offs made in this design

  • Security vs. User Experience: Using longer-lived tokens improves UX but increases security surface. Shorter tokens provide better security but may cause more authentication interruptions.
  • Performance vs. Security: Token validation adds processing overhead on every request, but removing it would make the application more vulnerable to CSRF attacks.
  • Complexity vs. Coverage: Implementing comprehensive CSRF protection across all request types adds complexity but ensures complete coverage of attack vectors.
  • State Management vs. Token Storage: Storing tokens in React context provides easy access but may conflict with server-side rendering or client-side token management strategies.

Risks and mitigations

  • Token Expiration Risk: If tokens expire too quickly, users may face authentication interruptions during long sessions. Mitigation: Implement token refresh mechanisms with grace periods and proper error handling.
  • Cross-Origin Request Risk: If not properly configured, CSRF protection may break legitimate cross-origin requests. Mitigation: Implement proper CORS headers and token validation for cross-origin scenarios.
  • Token Invalidation Risk: If tokens are not properly invalidated on session changes, they could be reused maliciously. Mitigation: Implement proper token invalidation on logout and session changes.
  • State Synchronization Risk: Client-side token state may get out of sync with server state. Mitigation: Implement robust error handling and automatic recovery mechanisms.

Technical debt acknowledged

  • Token Management Overhead: The current implementation requires additional state management and validation logic that could be simplified with better backend integration or token generation strategies.
  • Testing Coverage Gap: While the core CSRF protection logic is covered, edge cases around token expiration and cross-origin scenarios may require additional test coverage that isn't fully addressed in this design.
  • Framework Dependency: The current implementation assumes React context and hooks, which could create challenges if the application later migrates to a different frontend framework or state management system.
  • Session Management Coupling: The CSRF protection implementation is tightly coupled to session management, which could create maintenance challenges if these systems evolve independently.

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.

  • Frontend LLD

    Client-Side Authentication Manager Design

    Comprehensive low-level design for a secure, scalable client-side authentication system with token management and session handling.

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.