Skip to main content

Frontend Route Protection Design Patterns and Implementation

Comprehensive low-level design for implementing robust route protection mechanisms in React applications with authentication and authorization controls.

Route protection is a critical frontend concern that ensures users can only access authorized application sections based on their permissions and authentication status. This document presents a comprehensive low-level design approach for implementing secure routing patterns in modern React applications.

1. Problem Statement & Requirements

Clarifying questions

  1. What authentication method will be used (JWT, OAuth, SAML, etc.) and what are the token lifecycle requirements?
  2. Will this route protection apply to all routes or only specific protected routes, and how are those determined?
  3. What is the expected behavior when a user accesses a protected route without proper authentication (redirect to login, show unauthorized page, etc.)?
  4. Do we need to support role-based access control (RBAC) in addition to basic authentication?
  5. Should the route protection work for both client-side navigation and direct URL access (e.g., copy-paste)?
  6. Are there specific browser or device support requirements (mobile-first, older browsers like IE11)?
  7. How should the system handle cases where authentication tokens expire during user session?

Functional requirements

  • Route protection must prevent unauthorized access to protected routes
  • Users must be redirected to login page when accessing protected routes without valid authentication
  • Authentication state must be maintained across page refreshes and browser sessions
  • Protected routes should only be accessible by users with appropriate permissions (if RBAC is required)
  • The system must handle authentication token expiration gracefully
  • Users should be redirected back to the originally requested route after successful login

Non-functional requirements

  • Performance: Route protection checks must complete in under 50ms to avoid perceived lag
  • Accessibility: Redirects and error states must be fully accessible via keyboard and screen readers
  • Scalability: Route protection logic should scale to support hundreds of protected routes
  • Security: Authentication state must not be easily spoofable or manipulated by client-side code
  • Reliability: Route protection should work consistently across all supported browsers and devices

Constraints and assumptions

  • Authentication state is managed via JWT tokens stored in HTTP-only cookies or localStorage (depending on security requirements)
  • The application uses React Router v6+ with React 18+
  • Authentication status is determined by checking token validity and user permissions
  • Protected routes are identified by a protected flag or route configuration in routing setup
  • The system assumes that authentication state is available at the application root level
  • Route protection logic must be implemented in a way that works for both client-side and server-side rendering contexts

Out of scope

  • Authentication token refresh mechanisms (handled by separate auth service)
  • User session management beyond basic authentication state
  • Multi-factor authentication (MFA) integration
  • Third-party authentication provider integrations (Google, GitHub, etc.)
  • Server-side route protection (this is a frontend-only design)
  • Complex permission hierarchies beyond basic role-based access control
  • Integration with backend authentication endpoints (assumes existing auth service)

2. Component Architecture

Component Tree Diagram

graph TD
    A[App] --> B[Router]
    B --> C[ProtectedRoute]
    B --> D[PublicRoute]
    C --> E[AuthLayout]
    E --> F[Navbar]
    E --> G[MainContent]
    G --> H[Dashboard]
    G --> I[Profile]
    G --> J[Settings]
    D --> K[PublicLayout]
    K --> L[Login]
    K --> M[Register]
    K --> N[Home]

Component Breakdown

Smart Components (Container Components):

  • ProtectedRoute - Handles authentication checks and redirects
  • AuthLayout - Manages authenticated user layout with navigation
  • Dashboard - Main authenticated content container
  • Profile - User profile management container
  • Settings - Configuration management container

Dumb Components (Presentational Components):

  • Navbar - Navigation bar with links
  • Login - Authentication form component
  • Register - Registration form component
  • Home - Public landing page
  • NotFound - 404 error display component

TypeScript Props Contracts

// ProtectedRoute Props
interface ProtectedRouteProps {
  children: React.ReactNode;
  redirectPath?: string;
}

// AuthLayout Props
interface AuthLayoutProps {
  children: React.ReactNode;
  user?: User | null;
  isLoading: boolean;
  onLogout: () => void;
}

// Navbar Props
interface NavbarProps {
  user?: User | null;
  onLogout: () => void;
  activeRoute: string;
}

UI State Machine

stateDiagram-v2
    [*] --> Idle
    Idle --> CheckingAuth : CheckAuth()
    CheckingAuth --> Authenticated : AuthSuccess
    CheckingAuth --> Unauthenticated : AuthFailed
    Unauthenticated --> Redirecting : RedirectToLogin
    Redirecting --> Login : NavigateToLogin
    Authenticated --> Rendering : RenderContent
    Rendering --> Idle : Logout()
    Idle --> CheckingAuth : CheckAuth()

Component Responsibilities and Props

ProtectedRoute:

  • Responsible for protecting authenticated routes
  • Checks authentication status before rendering children
  • Redirects unauthenticated users to login page
  • Props: children (ReactNode), redirectPath (string)

AuthLayout:

  • Provides consistent authenticated layout structure
  • Manages user session state and loading states
  • Handles logout functionality
  • Props: children (ReactNode), user (User | null), isLoading (boolean), onLogout (function)

Navbar:

  • Displays navigation links for authenticated users
  • Handles logout and route navigation
  • Props: user (User | null), onLogout (function), activeRoute (string)

Reusability Decisions

  1. ProtectedRoute is highly reusable across different authenticated routes
  2. AuthLayout provides consistent layout for all authenticated pages
  3. Navbar is reused across multiple authenticated views
  4. Login/Register forms are kept as reusable components but can be customized per route

Component Lifecycle Considerations

  1. ProtectedRoute lifecycle:

    • Mounts and immediately checks authentication
    • Handles redirect on auth failure
    • Unmounts when route changes
  2. AuthLayout lifecycle:

    • Mounts with initial auth state
    • Handles auth status changes via useEffect
    • Cleans up auth listeners on unmount
  3. Navbar lifecycle:

    • Mounts with active route state
    • Updates based on route changes
    • Handles user session updates

The component architecture follows the principle of separation of concerns, with protected routes handling authentication logic while layout components manage presentation and navigation structure. The design supports both authenticated and public routes with clear component boundaries and type safety through TypeScript interfaces.

3. State Management Design

Local state vs. global state decisions

For route protection, we need to manage authentication state and navigation flow. Authentication status (logged in/out) is global across the application, while route-specific loading states and error messages can be local to individual components.

State management approach

We'll use React Context API + Zustand for state management:

  • Context API for authentication context (user, token, permissions)
  • Zustand for route protection state and navigation flow
  • React Query for API data fetching with automatic caching

Justification:

  • Context API provides clean authentication state access across components
  • Zustand offers minimal boilerplate for complex navigation flows without Redux's complexity
  • React Query handles API caching and revalidation efficiently

State shape and normalization strategy

// Authentication state
interface AuthState {
  isAuthenticated: boolean;
  user: User | null;
  token: string | null;
  permissions: string[];
}

// Route protection state
interface RouteProtectionState {
  currentRoute: string | null;
  redirectPath: string | null;
  isRedirecting: boolean;
  authStatus: 'idle' | 'loading' | 'authenticated' | 'unauthorized';
  error: string | null;
}

// Combined state
interface AppState {
  auth: AuthState;
  routeProtection: RouteProtectionState;
}

Server state vs. client state separation

  • Server state: User authentication status, permissions, protected route metadata
  • Client state: Current navigation flow, redirect paths, loading indicators

Cache invalidation strategy

  • Token expiration triggers cache invalidation
  • Manual logout clears all cached auth data
  • Protected route access checks refresh token when needed

Optimistic updates design

When a user logs in:

  1. Update local state optimistically
  2. Make API call to validate credentials
  3. If successful, update global auth context
  4. If failed, revert local state and show error

Critical-path hook implementation

// src/hooks/useRouteProtection.ts
import { useState, useEffect } from 'react';
import { useAuthStore } from '@/stores/authStore';
import { useNavigate } from 'react-router-dom';

interface RouteProtectionHook {
  isProtectedRoute: boolean;
  isAuthenticated: boolean;
  isLoading: boolean;
  error: string | null;
  redirectToLogin: () => void;
  checkRouteAccess: () => Promise<boolean>;
}

export const useRouteProtection = (routePath: string): RouteProtectionHook => {
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const navigate = useNavigate();
  
  const {
    isAuthenticated,
    user,
    token,
    permissions,
    login,
    logout
  } = useAuthStore();

  const checkRouteAccess = async (): Promise<boolean> => {
    try {
      setIsLoading(true);
      
      // Simulate API call to validate route access
      const response = await fetch(`/api/validate-route-access`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${token}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ route: routePath })
      });

      if (!response.ok) {
        throw new Error('Route access denied');
      }

      const { allowed, requiredPermissions } = await response.json();
      
      if (!allowed && requiredPermissions.length > 0) {
        setError(`Access denied. Required permissions: ${requiredPermissions.join(', ')}`);
        return false;
      }

      return true;
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to validate route access');
      return false;
    } finally {
      setIsLoading(false);
    }
  };

  const redirectToLogin = () => {
    navigate('/login', { 
      state: { from: routePath },
      replace: true 
    });
  };

  useEffect(() => {
    const validateRoute = async () => {
      if (!isAuthenticated) {
        redirectToLogin();
        return;
      }

      const hasAccess = await checkRouteAccess();
      if (!hasAccess) {
        redirectToLogin();
      }
    };

    validateRoute();
  }, [routePath, isAuthenticated]);

  return {
    isProtectedRoute: true,
    isAuthenticated,
    isLoading,
    error,
    redirectToLogin,
    checkRouteAccess
  };
};

// src/stores/authStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface AuthStore {
  isAuthenticated: boolean;
  user: User | null;
  token: string | null;
  permissions: string[];
  
  login: (credentials: { email: string; password: string }) => Promise<void>;
  logout: () => void;
  refreshAuth: () => Promise<boolean>;
  updatePermissions: (permissions: string[]) => void;
}

export const useAuthStore = create<AuthStore>()(
  persist(
    (set, get) => ({
      isAuthenticated: false,
      user: null,
      token: null,
      permissions: [],
      
      login: async (credentials) => {
        try {
          const response = await fetch('/api/auth/login', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(credentials)
          });
          
          const data = await response.json();
          
          if (!response.ok) {
            throw new Error(data.error || 'Login failed');
          }
          
          set({
            isAuthenticated: true,
            user: data.user,
            token: data.token,
            permissions: data.permissions || []
          });
          
        } catch (error) {
          set({ isAuthenticated: false, user: null, token: null });
          throw error;
        }
      },
      
      logout: () => {
        set({
          isAuthenticated: false,
          user: null,
          token: null,
          permissions: []
        });
      },
      
      refreshAuth: async () => {
        try {
          const response = await fetch('/api/auth/refresh', {
            method: 'POST',
            headers: { 
              'Authorization': `Bearer ${get().token}`,
              'Content-Type': 'application/json'
            }
          });
          
          const data = await response.json();
          
          if (response.ok) {
            set({
              isAuthenticated: true,
              user: data.user,
              token: data.token,
              permissions: data.permissions || []
            });
            return true;
          }
          
          set({ isAuthenticated: false, user: null, token: null });
          return false;
        } catch {
          set({ isAuthenticated: false, user: null, token: null });
          return false;
        }
      },
      
      updatePermissions: (permissions) => {
        set({ permissions });
      }
    }),
    {
      name: 'auth-storage',
      partialize: (state) => ({
        isAuthenticated: state.isAuthenticated,
        user: state.user,
        token: state.token,
        permissions: state.permissions
      })
    }
  )
);

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]
    G --> J[/dashboard/analytics]
    G --> K[/dashboard/reports]

The route structure follows a clear separation between public and protected routes:

  • Public Routes: /login, /register, /forgot-password - accessible without authentication
  • Protected Routes: /dashboard, /profile, /settings - require valid authentication
  • Nested Protected Routes: /dashboard/analytics, /dashboard/reports - child routes of protected parent

Code Splitting Strategy

Route-based code splitting with React.lazy and Suspense:

// routes/index.tsx
import { lazy, Suspense } from 'react';
import { Route, Routes, Navigate } from 'react-router-dom';

// Public routes - no code splitting needed as they're small
const Login = lazy(() => import('../features/auth/Login'));
const Register = lazy(() => import('../features/auth/Register'));
const ForgotPassword = lazy(() => import('../features/auth/ForgotPassword'));

// Protected routes - code split for better bundle size
const Dashboard = lazy(() => import('../features/dashboard/Dashboard'));
const Profile = lazy(() => import('../features/profile/Profile'));
const Settings = lazy(() => import('../features/settings/Settings'));

// Protected nested routes
const Analytics = lazy(() => import('../features/dashboard/Analytics'));
const Reports = lazy(() => import('../features/dashboard/Reports'));

export const AppRoutes = () => {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/login" element={<Login />} />
        <Route path="/register" element={<Register />} />
        <Route path="/forgot-password" element={<ForgotPassword />} />
        
        <Route element={<ProtectedRoute />}>
          <Route path="/dashboard" element={<Dashboard />}>
            <Route path="analytics" element={<Analytics />} />
            <Route path="reports" element={<Reports />} />
          </Route>
          <Route path="/profile" element={<Profile />} />
          <Route path="/settings" element={<Settings />} />
        </Route>
        
        <Route path="*" element={<Navigate to="/dashboard" replace />} />
      </Routes>
    </Suspense>
  );
};

Justification:

  • Public routes are small and frequently accessed, so code splitting overhead isn't worth it
  • Protected routes are larger and less frequently accessed, making code splitting beneficial for initial load time
// components/ProtectedRoute.tsx
import { useEffect, useState } from 'react';
import { useNavigate, Outlet } from 'react-router-dom';
import { useAuthStore } from '../stores/authStore';

export const ProtectedRoute = () => {
  const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
  const navigate = useNavigate();
  const { user, token, isLoading } = useAuthStore();

  useEffect(() => {
    if (isLoading) return;

    if (!token || !user) {
      // Redirect to login page with return URL
      const redirectUrl = window.location.pathname + window.location.search;
      navigate(`/login?redirect=${encodeURIComponent(redirectUrl)}`);
      setIsAuthenticated(false);
    } else {
      setIsAuthenticated(true);
    }
  }, [token, user, isLoading, navigate]);

  if (isAuthenticated === null) {
    return <div>Loading...</div>;
  }

  return isAuthenticated ? <Outlet /> : null;
};

// AuthGuard with token refresh logic
export const AuthGuard = () => {
  const navigate = useNavigate();
  const { token, refreshToken, refreshAuth } = useAuthStore();

  useEffect(() => {
    const handleTokenRefresh = async () => {
      if (refreshToken) {
        try {
          await refreshAuth();
        } catch (error) {
          // Token refresh failed - redirect to login
          navigate('/login');
        }
      } else {
        navigate('/login');
      }
    };

    if (token && isTokenExpired(token)) {
      handleTokenRefresh();
    }
  }, [token, refreshToken, navigate]);

  return <Outlet />;
};

Deep Linking Support and URL Parameter Design

// types/route-types.ts
export interface DashboardRouteParams {
  dashboardId?: string;
}

export interface ProfileRouteParams {
  userId?: string;
}

export interface SettingsRouteParams {
  section?: 'account' | 'notifications' | 'billing';
}

// URL patterns:
// /dashboard?tab=analytics&filter=date
// /profile/123?view=overview
// /settings/billing?plan=premium

export const buildDashboardUrl = (params: {
  tab?: string;
  filter?: string;
  dashboardId?: string;
}) => {
  const searchParams = new URLSearchParams();
  
  if (params.tab) searchParams.append('tab', params.tab);
  if (params.filter) searchParams.append('filter', params.filter);
  
  const baseUrl = params.dashboardId 
    ? `/dashboard/${params.dashboardId}`
    : '/dashboard';
    
  return `${baseUrl}?${searchParams.toString()}`;
};

export const buildProfileUrl = (userId: string, view?: string) => {
  const searchParams = new URLSearchParams();
  if (view) searchParams.append('view', view);
  
  return `/profile/${userId}?${searchParams.toString()}`;
};

Browser History Management and Scroll Restoration

// components/ScrollToTop.tsx
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

export const ScrollToTop = () => {
  const { pathname } = useLocation();

  useEffect(() => {
    window.scrollTo(0, 0);
  }, [pathname]);

  return null;
};

// Enhanced router with scroll restoration
import { BrowserRouter } from 'react-router-dom';

export const AppRouter = () => {
  return (
    <BrowserRouter 
      future={{ 
        v7_startTransition: true,
        v7_relativeSplatPath: true
      }}
      scrollBehavior="smooth"
    >
      <ScrollToTop />
      <AppRoutes />
    </BrowserRouter>
  );
};

Route Protection Implementation Strategy

The route protection system uses a layered approach:

  1. Route-level guards: ProtectedRoute component that checks authentication status
  2. Token validation: Periodic token expiration checks with automatic refresh
  3. Redirect handling: Preserves return URLs for seamless navigation
  4. Graceful degradation: Fallback to login page on token refresh failure

Key implementation details:

  • Uses React Router v6's useNavigate for programmatic navigation
  • Implements proper loading states during authentication checks
  • Handles token refresh with retry logic for network failures
  • Preserves URL parameters and query strings during redirects
  • Supports both session-based and JWT-based authentication flows

Performance considerations:

  • Route code splitting reduces initial bundle size
  • Suspense fallback provides better UX during lazy loading
  • Scroll restoration prevents jarring page jumps
  • History management ensures proper browser back/forward behavior

The design balances security with user experience, ensuring protected routes are only accessible to authenticated users while maintaining good performance and navigation semantics.

5. API Integration Layer

API Client Design Pattern

We'll implement a centralized API client using Axios with request/response interceptors for consistent handling of authentication, error responses, and data transformation. The client will be built as a singleton with proper configuration:

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

class ApiClient {
  private client: AxiosInstance;

  constructor() {
    this.client = axios.create({
      baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
      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 normalization
    this.client.interceptors.response.use(
      (response) => response,
      (error) => {
        if (error.response?.status === 401) {
          // Handle unauthorized access
          localStorage.removeItem('authToken');
          window.location.href = '/login';
        }
        return Promise.reject(error);
      }
    );
  }

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

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

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

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

export const apiClient = new ApiClient();

Request/Response Interceptors

The interceptors handle:

  1. Authentication: Injects auth tokens in headers for protected routes
  2. Error normalization: Transforms API errors to consistent format
  3. Automatic redirects: Redirects to login on 401 responses

Error Handling and Retry Strategy

We implement a retry mechanism with exponential backoff for transient network issues:

// api/retry.ts
export const retryWithBackoff = async <T>(
  fn: () => Promise<T>,
  retries = 3,
  delay = 1000
): Promise<T> => {
  try {
    return await fn();
  } catch (error) {
    if (retries > 0 && isRetryableError(error)) {
      await new Promise((resolve) => setTimeout(resolve, delay));
      return retryWithBackoff(fn, retries - 1, delay * 2);
    }
    throw error;
  }
};

const isRetryableError = (error: any): boolean => {
  return (
    error.code === 'ECONNABORTED' ||
    error.response?.status === 502 ||
    error.response?.status === 503 ||
    error.response?.status === 504
  );
};

Loading, Error, and Empty States Design

We implement a standardized loading state pattern using React Query's built-in loading states:

// components/LoadingState.tsx
import { Spinner } from './Spinner';

interface LoadingStateProps {
  isLoading: boolean;
  error?: Error | null;
  children: React.ReactNode;
}

export const LoadingState = ({ isLoading, error, children }: LoadingStateProps) => {
  if (isLoading) return <Spinner size="medium" />;
  
  if (error) {
    return (
      <div className="error-container">
        <p>Something went wrong</p>
        <button onClick={() => window.location.reload()}>Retry</button>
      </div>
    );
  }

  return <>{children}</>;
};

Data Fetching Strategy

We use a hybrid approach:

  • Protected routes: CSR with React Query for client-side data fetching
  • Public routes: SSG where appropriate (e.g., marketing pages)
  • Dynamic content: CSR with proper loading states and caching

For protected routes, we use React Query's useQuery hook with automatic revalidation:

// hooks/useUserData.ts
import { useQuery } from '@tanstack/react-query';
import { apiClient } from '../api/client';

interface User {
  id: string;
  name: string;
  email: string;
}

export const useUserData = () => {
  return useQuery<User>({
    queryKey: ['user'],
    queryFn: async () => {
      const response = await apiClient.get<User>('/api/user');
      return response.data;
    },
    staleTime: 5 * 60 * 1000, // 5 minutes
    cacheTime: 10 * 60 * 1000, // 10 minutes
  });
};

API Response Transformation Layer

We implement a DTO-to-domain model mapping layer:

// api/mappers.ts
import { User as DomainUser } from '../domain/models';
import { UserDTO } from './dtos';

export const mapUserDtoToDomain = (dto: UserDTO): DomainUser => {
  return {
    id: dto.id,
    name: dto.name,
    email: dto.email,
    createdAt: new Date(dto.created_at),
  };
};

// api/dtos.ts
export interface UserDTO {
  id: string;
  name: string;
  email: string;
  created_at: string;
}

Sequence Diagram for Primary Data Flows

sequenceDiagram
    participant U as User
    participant R as Router/ProtectedRoute
    participant Q as React Query
    participant A as ApiClient
    participant S as Server

    U->>R: Navigate to protected route
    R->>Q: Fetch user data with useQuery
    Q->>A: Make API request
    A->>S: Send authenticated request
    S-->>A: Return user data
    A-->>Q: Pass response to query
    Q-->>R: Provide user data or loading state
    R->>U: Render UI with user data

The sequence shows the typical flow for protected route data fetching, demonstrating how authentication is handled through the interceptor and how React Query manages the caching and loading states.

6. Performance Design

Core Web Vitals Targets

  • Largest Contentful Paint (LCP): <2.5s for 95% of users
  • First Input Delay (FID): <100ms for 95% of users
  • Cumulative Layout Shift (CLS): <0.1 for 95% of users

Bundle Size Budget per Route

  • Public routes (landing, login, signup): <200KB total (including vendor bundles)
  • Protected routes (dashboard, profile): <400KB total
  • Feature-specific routes (analytics, settings): <300KB each

Critical Rendering Path Optimization

  • Critical CSS inlined for landing page and auth flows (4KB max)
  • CSS loading strategy:
    • Critical styles loaded inline in HTML head
    • Non-critical styles loaded via CSS-in-JS or deferred loading
  • JavaScript chunking: Route-based code splitting with preloading of critical chunks

Image Optimization Strategy

  • Formats: WebP for modern browsers, JPEG fallback for legacy
  • Lazy loading: loading="lazy" attribute with IntersectionObserver polyfill
  • Responsive images: srcset and sizes attributes for different breakpoints
  • Image compression: 20% smaller than original with minimal perceptual loss

Font Loading Strategy

  • Preload critical font subsets (latin, emoji) via <link rel="preload">
  • Font display strategy: font-display: swap to prevent FOIT
  • Custom font loading: Fallback to system fonts while custom fonts load
  • Subset font files: Only load characters needed for current route

Memoization Strategy

What to memoize:

  • useCallback for event handlers passed down to child components
  • useMemo for expensive calculations (e.g., filtered lists, computed values)
  • React.memo for presentational components with stable props

What NOT to memoize:

  • Simple state updates (e.g., useState setter functions)
  • Props that change frequently (e.g., user object with session token)
  • Components with no performance impact

Virtualization for Large Lists

// Example 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]}
    </div>
  );

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

Prefetching and Preloading Strategy

  • Route preloading: Critical route chunks (e.g., dashboard, profile) preloaded after login
  • Data prefetching: User preferences and basic dashboard data prefetched on auth success
  • Link prefetching: Preload next route's chunks when user hovers over navigation links
  • Service worker caching: Cache static assets and API responses for offline access

Performance Monitoring

  • Lighthouse score targets: 90+ for all core metrics (LCP, FID, CLS)
  • Real User Monitoring: Track actual performance in production using RUM tools
  • Bundle size tracking: Monitor bundle size changes with each deployment
  • Error boundary monitoring: Track performance degradation through error reporting

7. Error Handling & Resilience

Error Boundary Placement Strategy

Error boundaries should be strategically placed to provide the best user experience while maintaining system stability. The placement follows a hierarchical approach:

  1. Application-wide error boundary - Covers entire app for unhandled errors
  2. Route-level error boundaries - Handle route-specific failures (e.g., data fetch errors)
  3. Component-level error boundaries - Handle specific UI component failures (e.g., chart rendering errors)
  4. Feature-specific error boundaries - For complex nested components (e.g., dashboard widgets)
graph TD
    A[App Root] --> B[ErrorBoundary]
    B --> C[Public Routes]
    B --> D[Protected Routes]
    B --> E[Layout Components]
    C --> F[HomeRoute]
    D --> G[DashboardRoute]
    D --> H[ProfileRoute]
    G --> I[WidgetErrorBoundary]
    G --> J[ChartComponent]

Global Error Handler Design

A centralized error handler manages application-wide errors with structured logging and user feedback:

// src/core/error/ErrorService.ts
import { Logger } from './Logger';

export interface ErrorContext {
  component?: string;
  operation?: string;
  userId?: string;
  route?: string;
}

export class ErrorService {
  private logger = new Logger();
  private sentryEnabled = process.env.NODE_ENV === 'production';

  handleError(error: Error, context: ErrorContext = {}): void {
    const errorInfo = {
      message: error.message,
      stack: error.stack,
      timestamp: new Date().toISOString(),
      ...context
    };

    this.logger.error('Unhandled error occurred', errorInfo);

    if (this.sentryEnabled) {
      // Sentry integration would be here
      // Sentry.captureException(error, { contexts: errorInfo });
    }

    // Show user-friendly error message
    this.showUserError(error, context);
  }

  showUserError(error: Error, context: ErrorContext): void {
    // Display toast or error modal based on error type
    const userMessage = this.getUserFriendlyMessage(error, context);
    
    // In a real implementation, this would trigger a toast notification
    console.error(`[User Error]: ${userMessage}`);
  }

  private getUserFriendlyMessage(error: Error, context: ErrorContext): string {
    if (error.name === 'NetworkError') return 'Network connection lost. Please check your internet.';
    if (error.name === 'AuthenticationError') return 'Session expired. Please sign in again.';
    if (error.name === 'AuthorizationError') return 'Access denied. You do not have permission to view this content.';
    return 'An unexpected error occurred. Please try again later.';
  }
}

Offline / Network Failure Handling

Offline resilience is implemented through network status monitoring and graceful degradation:

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

export interface NetworkStatus {
  online: boolean;
  status: 'online' | 'offline';
  lastUpdated: Date;
}

export class NetworkService {
  private static instance: NetworkService;
  private status: NetworkStatus = { online: true, status: 'online', lastUpdated: new Date() };
  private listeners: Array<(status: NetworkStatus) => void> = [];

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

  getNetworkStatus(): NetworkStatus {
    return this.status;
  }

  subscribe(listener: (status: NetworkStatus) => void): () => void {
    this.listeners.push(listener);
    
    // Immediately notify with current status
    listener(this.status);
    
    return () => {
      const index = this.listeners.indexOf(listener);
      if (index > -1) {
        this.listeners.splice(index, 1);
      }
    };
  }

  private notifyListeners(status: NetworkStatus): void {
    this.listeners.forEach(listener => listener(status));
  }

  // Initialize network monitoring
  init(): void {
    if (typeof window !== 'undefined' && 'navigator' in window) {
      const handleOnline = () => {
        this.status = { online: true, status: 'online', lastUpdated: new Date() };
        this.notifyListeners(this.status);
      };

      const handleOffline = () => {
        this.status = { online: false, status: 'offline', lastUpdated: new Date() };
        this.notifyListeners(this.status);
      };

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

      // Cleanup function to remove event listeners
      return () => {
        window.removeEventListener('online', handleOnline);
        window.removeEventListener('offline', handleOffline);
      };
    }
  }
}

Graceful Degradation Patterns

The system implements progressive enhancement for different failure scenarios:

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

interface OfflineFallbackProps {
  title?: string;
  message?: string;
  retryAction?: () => void;
}

export const OfflineFallback: React.FC<OfflineFallbackProps> = ({
  title = 'Offline Mode',
  message = 'You are currently offline. Some features may be limited.',
  retryAction
}) => {
  return (
    <div className="offline-fallback">
      <h2>{title}</h2>
      <p>{message}</p>
      {retryAction && (
        <button onClick={retryAction} className="retry-button">
          Retry Connection
        </button>
      )}
    </div>
  );
};

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

interface CacheFallbackProps {
  fallbackData?: any;
  onRetry?: () => void;
}

export const CacheFallback: React.FC<CacheFallbackProps> = ({
  fallbackData,
  onRetry
}) => {
  if (!fallbackData) {
    return <div>No cached data available</div>;
  }

  return (
    <div className="cache-fallback">
      <p>Showing cached data due to network issues</p>
      {onRetry && (
        <button onClick={onRetry} className="retry-button">
          Refresh Data
        </button>
      )}
    </div>
  );
};

User Feedback on Errors

Comprehensive error feedback mechanisms ensure users understand what went wrong and how to recover:

// src/core/error/ToastManager.tsx
import React from 'react';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

export interface ToastOptions {
  type?: 'success' | 'error' | 'info' | 'warning';
  autoClose?: number;
  position?: 'top-right' | 'top-center' | 'top-left' | 'bottom-right' | 'bottom-center' | 'bottom-left';
  hideProgressBar?: boolean;
  closeOnClick?: boolean;
  pauseOnHover?: boolean;
  draggable?: boolean;
}

export class ToastManager {
  static show(message: string, options: ToastOptions = {}): void {
    const defaultOptions: ToastOptions = {
      autoClose: 5000,
      position: 'top-right',
      hideProgressBar: false,
      closeOnClick: true,
      pauseOnHover: true,
      draggable: true
    };

    toast(message, { ...defaultOptions, ...options });
  }

  static showError(message: string, options?: ToastOptions): void {
    this.show(message, { ...options, type: 'error' });
  }

  static showSuccess(message: string, options?: ToastOptions): void {
    this.show(message, { ...options, type: 'success' });
  }

  static showWarning(message: string, options?: ToastOptions): void {
    this.show(message, { ...options, type: 'warning' });
  }
}

// ToastContainer component to be included in App root
export const ToastContainerWrapper: React.FC = () => {
  return (
    <ToastContainer 
      position="top-right"
      autoClose={5000}
      hideProgressBar={false}
      newestOnTop={false}
      closeOnClick
      rtl={false}
      pauseOnFocusLoss
      draggable
      pauseOnHover
    />
  );
};

Error Boundary Implementation

// src/core/error/AppErrorBoundary.tsx
import React from 'react';
import { ErrorService } from './ErrorService';
import { OfflineFallback } from './OfflineFallback';

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

class AppErrorBoundary extends React.Component<React.PropsWithChildren<{}>, AppErrorBoundaryState> {
  private errorService = new ErrorService();

  constructor(props: React.PropsWithChildren<{}>) {
    super(props);
    this.state = { hasError: false };
  }

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

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
    this.errorService.handleError(error, {
      component: 'AppErrorBoundary',
      operation: 'componentDidCatch'
    });

    this.setState({ errorInfo });
  }

  render(): React.ReactNode {
    if (this.state.hasError) {
      // For network errors, show offline fallback
      if (this.state.error?.name === 'NetworkError') {
        return <OfflineFallback retryAction={() => window.location.reload()} />;
      }

      // For other errors, show generic error UI
      return (
        <div className="error-boundary">
          <h2>Something went wrong</h2>
          <p>We're sorry, but an error occurred while rendering this page.</p>
          <button onClick={() => window.location.reload()}>
            Reload Page
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

export default AppErrorBoundary;

Retry Strategy Implementation

// src/core/error/RetryableRequest.ts
import { NetworkService } from './NetworkService';

export interface RetryConfig {
  maxRetries?: number;
  delay?: number;
  exponentialBackoff?: boolean;
}

export class RetryableRequest {
  private networkService = NetworkService.getInstance();
  
  async executeWithRetry<T>(
    request: () => Promise<T>,
    config: RetryConfig = {}
  ): Promise<T> {
    const {
      maxRetries = 3,
      delay = 1000,
      exponentialBackoff = true
    } = config;

    let lastError: Error;
    
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        // Check network status before making request
        const networkStatus = this.networkService.getNetworkStatus();
        if (!networkStatus.online && attempt > 0) {
          throw new Error('Network offline');
        }

        const result = await request();
        return result;
      } catch (error) {
        lastError = error as Error;
        
        // Don't retry on authentication errors or client-side errors
        if (this.shouldNotRetry(error)) {
          throw error;
        }

        // If we've exhausted retries, throw the last error
        if (attempt === maxRetries) {
          throw lastError;
        }

        // Calculate delay with exponential backoff
        const currentDelay = exponentialBackoff 
          ? delay * Math.pow(2, attempt) 
          : delay;

        // Wait before retrying
        await this.delay(currentDelay);
      }
    }

    throw lastError;
  }

  private shouldNotRetry(error: unknown): boolean {
    if (error instanceof Error) {
      return ['AuthenticationError', 'AuthorizationError', 'ValidationError'].includes(error.name);
    }
    return false;
  }

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

Error Recovery Patterns

The system implements automatic recovery for common failure modes:

// src/core/error/RecoveryManager.ts
import { NetworkService } from './NetworkService';
import { CacheService } from '../cache/CacheService';

export class RecoveryManager {
  private networkService = NetworkService.getInstance();
  private cacheService = new CacheService();

  async recoverFromNetworkError<T>(
    fetchFn: () => Promise<T>,
    cacheKey?: string
  ): Promise<T> {
    try {
      // Attempt to fetch fresh data
      const result = await fetchFn();
      
      // If successful, cache the result for future use
      if (cacheKey) {
        await this.cacheService.set(cacheKey, result);
      }
      
      return result;
    } catch (error) {
      // If network error, check if we have cached data
      if (cacheKey && this.isNetworkError(error)) {
        const cached = await this.cacheService.get<T>(cacheKey);
        if (cached) {
          return cached;
        }
      }

      // If no cache and still network error, throw the error
      throw error;
    }
  }

  private isNetworkError(error: unknown): boolean {
    return error instanceof Error && 
      (error.name === 'NetworkError' || 
       error.message.includes('network') ||
       error.message.includes('offline'));
  }
}

8. Testing Strategy

Unit Testing

  • Component testing: 90%+ coverage for all presentational and container components using React Testing Library
  • Hook testing: 100% coverage for custom hooks including edge cases and error states
  • Utility function testing: 95%+ coverage for all utility functions (auth helpers, route guards, etc.)
  • Test data strategy: Use fixtures and factories for consistent test data generation with realistic edge cases
  • Target coverage: 85%+ overall code coverage, with >90% for critical auth paths

Integration Testing Strategy

  • Route guard integration: Test protected route transitions with and without valid auth tokens
  • Navigation flow: Verify proper redirect behavior when accessing protected routes
  • Authentication state transitions: Test login → protected route → logout flow
  • Nested route protection: Validate child route protection inheritance and override behavior

E2E Testing Scope and Tool

  • Tool: Playwright (choosen for better TypeScript support, parallel test execution, and robust browser automation)
  • Covered flows:
    • Public route access (no auth required)
    • Protected route access with valid session
    • Protected route access without session (redirect to login)
    • Session timeout handling and re-authentication flow
    • Navigation between protected and public routes
  • Test scope: End-to-end user journey testing for authentication flows, focusing on 3 key scenarios:
    1. Anonymous user accessing protected route
    2. Authenticated user navigating between routes
    3. Session expiration and re-authentication

Visual Regression Testing

  • Tool: Chromatic (or Percy) for visual regression detection
  • Scope: All protected route layouts and authentication modals
  • Breakpoint coverage: Mobile, tablet, desktop viewports
  • Test strategy: Snapshot testing of key authenticated UI states to prevent visual regressions

Accessibility Testing Automation

  • Tool: axe-core with Cypress integration (or Playwright axe plugin)
  • Test scope: All protected routes and auth modals must pass WCAG 2.1 AA
  • Automated checks: Keyboard navigation, ARIA attribute validation, color contrast compliance
  • Manual verification: Screen reader testing for complex auth flows

Test Data Strategy and Fixtures

  • Fixtures: Create realistic user profiles, session tokens, and route configurations
  • Data isolation: Use unique test data per test run to prevent flaky tests
  • Mocking strategy:
    • Network requests: Use MSW (Mock Service Worker) for realistic API mocking
    • Auth state: Use context providers to simulate different auth states
  • Test environment: Isolated test databases and API endpoints for authentication flows

9. Accessibility (A11y)

WCAG 2.1 AA compliance checklist for this feature

The route protection system must meet WCAG 2.1 AA standards with specific attention to:

  • Keyboard Navigation: All protected routes and authentication flows must be fully operable via keyboard alone
  • Focus Management: Proper focus trapping and restoration when navigating between protected and public routes
  • Screen Reader Compatibility: ARIA labels and roles must be properly implemented for authentication modals and redirect flows
  • Error Handling: Error states during authentication must provide sufficient context for screen reader users
  • Contrast Ratios: All UI elements in authentication flows must meet 4.5:1 contrast ratio for normal text and 3:1 for large text

Keyboard navigation support

All protected route flows must support keyboard-only navigation:

  • Tab navigation through authentication form fields
  • Enter key submission for login forms
  • Escape key to close modals or cancel flows
  • Focus restoration when returning from protected routes

Focus management strategy

Focus management is critical for authentication flows:

stateDiagram-v2
    [*] --> PublicRoute
    PublicRoute --> AuthModal: User navigates to protected route
    AuthModal --> LoginField: Focus set to first input
    LoginField --> PasswordField: Tab navigation
    PasswordField --> SubmitButton: Tab navigation
    SubmitButton --> AuthModal: Form submission
    AuthModal --> ProtectedRoute: Successful auth
    ProtectedRoute --> PublicRoute: User logs out

ARIA roles and attributes plan

Authentication modals and protected route indicators will use:

  • role="dialog" for authentication modals
  • aria-modal="true" to indicate modal state
  • aria-labelledby for accessible labels
  • aria-describedby for form field descriptions
  • aria-live="polite" for status updates
  • tabindex="-1" to manage focus trapping

Screen reader compatibility

All protected route interactions must provide:

  • Clear announcements of authentication state changes
  • Contextual information about required permissions
  • Proper reading order for form elements in authentication flows
  • Landmark regions to help screen reader users navigate protected content
// Example of accessible route protection with ARIA attributes
const ProtectedRoute: React.FC<{ 
  component: React.ComponentType; 
  requiredPermissions?: string[] 
}> = ({ component: Component, requiredPermissions }) => {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [showAuthModal, setShowAuthModal] = useState(false);
  
  useEffect(() => {
    // Check authentication status
    const checkAuth = async () => {
      const authStatus = await checkUserAuthentication();
      setIsAuthenticated(authStatus);
    };
    
    checkAuth();
  }, []);

  const handleLogin = async (credentials: LoginCredentials) => {
    try {
      const result = await login(credentials);
      setIsAuthenticated(result.success);
      setShowAuthModal(false);
    } catch (error) {
      // Handle error with accessible feedback
      console.error('Login failed', error);
    }
  };

  if (!isAuthenticated) {
    return (
      <div 
        role="dialog" 
        aria-modal="true"
        aria-labelledby="auth-dialog-title"
        aria-describedby="auth-dialog-description"
        className="auth-modal"
      >
        <h2 id="auth-dialog-title">Authentication Required</h2>
        <p id="auth-dialog-description">
          This content is protected. Please sign in to continue.
        </p>
        <LoginForm onLogin={handleLogin} />
      </div>
    );
  }

  return <Component />;
};

Focus trapping implementation

Protected route flows will implement focus trapping to maintain keyboard accessibility:

// Focus trap utility for authentication modals
const useFocusTrap = (isOpen: boolean) => {
  const modalRef = useRef<HTMLDivElement>(null);
  
  useEffect(() => {
    if (!isOpen) return;
    
    const handleFocusTrap = (e: KeyboardEvent) => {
      if (e.key === 'Tab') {
        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;
        
        if (e.shiftKey && document.activeElement === firstElement) {
          lastElement.focus();
          e.preventDefault();
        } else if (!e.shiftKey && document.activeElement === lastElement) {
          firstElement.focus();
          e.preventDefault();
        }
      }
    };
    
    document.addEventListener('keydown', handleFocusTrap);
    return () => document.removeEventListener('keydown', handleFocusTrap);
  }, [isOpen]);
};

Alternative content and error states

All authentication flows must provide:

  • Clear error messages with sufficient context for screen readers
  • Visual and auditory feedback for authentication status changes
  • Properly labeled form fields with associated error messages
  • Alternative content when authentication fails or is interrupted

Keyboard-only navigation patterns

Protected route flows must support:

  • Complete keyboard navigation through all authentication steps
  • Proper tab order that follows logical content flow
  • Focus restoration to original location after successful authentication
  • Escape key support for closing modals and canceling flows
  • Form submission via Enter key without mouse interaction

10. Security Considerations

XSS Prevention Strategy

Content Sanitization: All user-generated content must be sanitized before rendering to prevent XSS attacks. Implement a robust HTML sanitizer using libraries like DOMPurify or sanitize-html.

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

const sanitizeHTML = (rawHTML: string): string => {
  return DOMPurify.sanitize(rawHTML, {
    ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li'],
    ALLOWED_ATTR: ['class'],
    ALLOW_DATA_ATTR: false,
  });
};

// Usage in component
const UserContent = ({ content }: { content: string }) => {
  const sanitizedContent = sanitizeHTML(content);
  return <div dangerouslySetInnerHTML={{ __html: sanitizedContent }} />;
};

Input Validation: All user inputs must be validated server-side and client-side with strict sanitization. Use regex patterns for specific fields and validate against known attack vectors.

CSRF Protection

Token-Based Protection: Implement CSRF tokens for all state-changing requests (POST, PUT, DELETE). Tokens must be:

  • Generated per session
  • Embedded in forms and headers
  • Validated server-side
// CSRF token implementation
const useCSRFToken = () => {
  const [csrfToken, setCsrfToken] = useState<string | null>(null);
  
  useEffect(() => {
    // Fetch CSRF token on mount
    const fetchToken = async () => {
      try {
        const response = await fetch('/api/csrf-token', {
          method: 'GET',
          credentials: 'include',
        });
        const data = await response.json();
        setCsrfToken(data.token);
      } catch (error) {
        console.error('Failed to fetch CSRF token:', error);
      }
    };
    
    fetchToken();
  }, []);
  
  return csrfToken;
};

// Usage in API requests
const makeSecureRequest = async (url: string, options: RequestInit) => {
  const token = useCSRFToken();
  const headers = {
    ...options.headers,
    'X-CSRF-Token': token,
  };
  
  return fetch(url, { ...options, headers });
};

Content Security Policy (CSP) Headers

Header Configuration: Implement strict CSP headers to prevent script injection and other attacks:

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

Sensitive Data Handling in Frontend State

Data Isolation: Never store sensitive authentication tokens or credentials in React state or localStorage. Use secure HTTP-only cookies for session management.

// Secure token handling pattern
const useAuthState = () => {
  // Never store sensitive tokens in React state
  const [user, setUser] = useState<User | null>(null);
  
  // Use secure cookie-based authentication
  useEffect(() => {
    const fetchUser = async () => {
      try {
        // Use HTTP-only cookies for auth tokens
        const response = await fetch('/api/auth/user', {
          credentials: 'include', // Important for cookies
        });
        
        if (response.ok) {
          const userData = await response.json();
          setUser(userData);
        }
      } catch (error) {
        console.error('Auth fetch failed:', error);
        setUser(null);
      }
    };
    
    fetchUser();
  }, []);
  
  return { user, isAuthenticated: !!user };
};

Input Sanitization Approach

Comprehensive Validation: Implement multi-layered input validation:

// Input sanitization utilities
const sanitizeInput = (input: string): string => {
  // Remove potentially dangerous characters
  return input
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
};

const validateEmail = (email: string): boolean => {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
};

const validatePassword = (password: string): boolean => {
  // At least 8 chars, one uppercase, lowercase, number, special char
  const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
  return passwordRegex.test(password);
};

// Usage in form validation
const validateForm = (formData: FormData) => {
  const errors: Record<string, string> = {};
  
  if (!validateEmail(formData.email)) {
    errors.email = 'Invalid email format';
  }
  
  if (!validatePassword(formData.password)) {
    errors.password = 'Password must be at least 8 characters with special chars';
  }
  
  return errors;
};

Additional Security Measures

Rate Limiting: Implement client-side rate limiting for API requests to prevent abuse.

Secure Headers: Configure security headers in server configuration:

  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • X-XSS-Protection: 1; mode=block

Secure Authentication Flow:

  • Use HTTPS exclusively
  • Implement proper session timeout handling
  • Never expose sensitive data in URLs or logs

11. Internationalization (i18n)

Translation Key Structure and Naming Convention

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

// Feature-scoped translation keys
{
  "auth": {
    "login": {
      "title": "Login to Your Account",
      "emailLabel": "Email Address",
      "passwordLabel": "Password",
      "submitButton": "Sign In"
    },
    "register": {
      "title": "Create New Account",
      "nameLabel": "Full Name"
    }
  },
  "common": {
    "buttons": {
      "save": "Save",
      "cancel": "Cancel",
      "delete": "Delete"
    },
    "errors": {
      "networkError": "Network error occurred",
      "validationError": "Please correct the form errors"
    }
  }
}

RTL Support Considerations

RTL support requires:

  • CSS direction property handling (direction: rtl)
  • Layout adjustments for right-aligned content
  • Component-specific RTL-aware styling with conditional classes
  • Right-to-left text alignment for form fields and labels

Date, Number, Currency Formatting Strategy

Using Intl API with locale-specific formatting:

// Format utilities
const formatDate = (date: Date, locale: string): string => {
  return new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }).format(date);
};

const formatCurrency = (amount: number, locale: string, currency: string): string => {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency
  }).format(amount);
};

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

Locale Detection and Switching Strategy

Automatic locale detection via browser settings with manual override:

// Locale detection logic
const detectLocale = (): string => {
  const stored = localStorage.getItem('locale');
  if (stored) return stored;
  
  const browserLang = navigator.language || 'en-US';
  const supported = ['en-US', 'fr-FR', 'es-ES', 'ar-SA'];
  
  // Match to supported locales or default to English
  return supported.find(lang => lang.startsWith(browserLang.split('-')[0])) || 'en-US';
};

// Locale switcher component
const LocaleSwitcher = () => {
  const [currentLocale, setCurrentLocale] = useState<string>(detectLocale());
  
  const handleSwitch = (newLocale: string) => {
    localStorage.setItem('locale', newLocale);
    i18n.changeLanguage(newLocale);
    // Update direction for RTL languages
    document.documentElement.dir = newLocale === 'ar-SA' ? 'rtl' : 'ltr';
  };
  
  return (
    <select value={currentLocale} onChange={(e) => handleSwitch(e.target.value)}>
      {supportedLocales.map(locale => (
        <option key={locale} value={locale}>{getLocaleName(locale)}</option>
      ))}
    </select>
  );
};

Translation Key Management

  • Centralized translation files per language (en-US.json, fr-FR.json)
  • Translation key validation using JSON schema
  • Automated translation key extraction from codebase for translation teams
  • Fallback mechanism to English when keys missing in other locales

Component-Level i18n Integration

Components consume translations through a custom hook:

// Translation hook
const useTranslation = (namespace: string) => {
  const { t, i18n } = useTranslation();
  
  return useCallback((key: string, params?: Record<string, any>) => {
    const fullKey = `${namespace}.${key}`;
    return t(fullKey, params);
  }, [t, namespace]);
};

// Usage in component
const LoginButton = () => {
  const t = useTranslation('auth.login');
  
  return (
    <button type="submit">
      {t('submitButton')}
    </button>
  );
};

i18n Implementation Considerations

  • Dynamic import of translation files to support code splitting
  • Fallback language handling for incomplete translations
  • Translation key caching to avoid redundant lookups
  • Integration with existing design system for consistent text styles across locales

12. Monitoring & Observability

Real User Monitoring (RUM) Metrics to Track

The frontend monitoring strategy focuses on Core Web Vitals and user interaction metrics:

  • Largest Contentful Paint (LCP): Target <2.5s for 95% of users, measured per route and user segment
  • First Input Delay (FID): Target <100ms for 95% of users, tracked per interaction type (click, scroll, etc.)
  • Cumulative Layout Shift (CLS): Target <0.1 for 95% of users, measured per page load
  • Navigation Time: Track time from route navigation to first contentful paint (FCP)
  • Error Rate: Per-route error count and percentage of total requests
  • Session Duration: Average session length to identify engagement patterns

Error Tracking Integration

Sentry Integration Strategy:

// src/monitoring/errorTracking.ts
import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing';

export const initSentry = () => {
  Sentry.init({
    dsn: process.env.REACT_APP_SENTRY_DSN,
    integrations: [
      new Integrations.BrowserTracing(),
    ],
    tracesSampleRate: 1.0,
    environment: process.env.REACT_APP_ENVIRONMENT,
    release: process.env.REACT_APP_RELEASE_VERSION,
  });
};

// Error boundary wrapper for route components
export const withErrorBoundary = (Component: React.ComponentType) => {
  return Sentry.withErrorBoundary(Component, {
    fallback: ErrorFallbackComponent,
    beforeSend: (error, event) => {
      // Filter out known non-critical errors
      if (error.name === 'AbortError') return null;
      return event;
    }
  });
};

Custom Events and User Journey Tracking

User Action Tracking:

// src/monitoring/userJourney.ts
type UserAction = {
  action: 'route_change' | 'auth_success' | 'auth_failure' | 'permission_denied';
  route?: string;
  timestamp: number;
  userId?: string;
  metadata?: Record<string, any>;
};

class UserJourneyTracker {
  private events: UserAction[] = [];
  
  track(action: UserAction) {
    this.events.push({
      ...action,
      timestamp: Date.now()
    });
    
    // Send to analytics service
    this.sendToAnalytics(action);
  }
  
  sendToAnalytics(event: UserAction) {
    // Implementation for sending to Mixpanel, Google Analytics, or custom backend
  }
}

export const journeyTracker = new UserJourneyTracker();

Performance Monitoring and Regression Detection

Performance Baseline Tracking:

// src/monitoring/performance.ts
type PerformanceMetrics = {
  lcp: number;
  fid: number;
  cls: number;
  navigationTime: number;
};

class PerformanceMonitor {
  private baselineMetrics: Partial<PerformanceMetrics> = {};
  
  async captureMetrics() {
    const metrics = await this.getCoreWebVitals();
    
    // Compare against baseline
    this.checkForRegression(metrics);
    
    return metrics;
  }
  
  private checkForRegression(metrics: PerformanceMetrics) {
    Object.entries(metrics).forEach(([key, current]) => {
      const baseline = this.baselineMetrics[key as keyof PerformanceMetrics];
      
      if (baseline && current > baseline * 1.2) {
        // Log performance regression
        Sentry.captureMessage(`Performance regression detected for ${key}: ${current} vs baseline ${baseline}`);
      }
    });
  }
  
  private async getCoreWebVitals() {
    // Implementation using Web Vitals API or Lighthouse
    return {
      lcp: 1500,
      fid: 80,
      cls: 0.05,
      navigationTime: 1200
    };
  }
}

export const performanceMonitor = new PerformanceMonitor();

Performance Alerting Configuration:

  • LCP > 2.5s: Alert threshold (30% increase from baseline)
  • FID > 100ms: Alert threshold (50% increase from baseline)
  • CLS > 0.1: Alert threshold (25% increase from baseline)
  • Navigation Time > 2s: Alert threshold (30% increase)

Integration Points

Monitoring Dashboard:

  • Real-time dashboard for route-level performance
  • User session replay capability for debugging
  • Error rate heatmaps by user segment and route
  • Custom event funnel tracking for authentication flow

Alerting Strategy:

  1. Critical Alerts: >50% increase in error rate or performance regressions
  2. Warning Alerts: >25% increase in performance metrics or user journey drops
  3. Info Alerts: New route added, new user segment detected

Data Retention:

  • 30 days for performance metrics
  • 90 days for error logs and user journey data
  • 1 year for aggregate analytics and trends

Observability Tools Integration

Sentry + DataDog:

// src/monitoring/integrations.ts
import * as Sentry from '@sentry/react';
import { DataDog } from './datadog';

class ObservabilityManager {
  init() {
    // Initialize Sentry for error tracking
    Sentry.init({
      dS: process.env.REACT_APP_SENTRY_DSN,
      // ... other Sentry config
    });
    
    // Initialize DataDog RUM for performance monitoring
    DataDog.init({
      clientToken: process.env.REACT_APP_DATADOG_CLIENT_TOKEN,
      site: 'datadoghq.com',
      service: 'frontend-app',
      env: process.env.REACT_APP_ENVIRONMENT,
      version: process.env.REACT_APP_RELEASE_VERSION,
    });
  }
  
  // Track custom performance metrics
  trackCustomMetric(name: string, value: number) {
    Sentry.setExtra(name, value);
    DataDog.addCustomMetric(name, value);
  }
}

User Journey Analytics

Authentication Flow Tracking:

// src/monitoring/authAnalytics.ts
type AuthFlowEvent = {
  flow: 'login' | 'signup' | 'reset_password';
  step: string;
  success: boolean;
  duration?: number;
  error?: string;
};

class AuthAnalytics {
  private flowEvents: AuthFlowEvent[] = [];
  
  trackAuthFlow(flow: 'login' | 'signup' | 'reset_password', step: string, success: boolean) {
    const event: AuthFlowEvent = {
      flow,
      step,
      success,
      timestamp: Date.now()
    };
    
    this.flowEvents.push(event);
    
    // Send to analytics backend
    this.sendAuthEvent(event);
  }
  
  sendAuthEvent(event: AuthFlowEvent) {
    // Implementation for sending to analytics service
    // Could include user ID, session ID, and device info
  }
}

This monitoring strategy provides comprehensive observability for route protection functionality, capturing both technical performance and user experience metrics to ensure the system behaves as expected under real-world conditions.

13. File & Folder Structure

Proposed Directory Structure

/src
├── app/
│   ├── layout.tsx
│   ├── page.tsx
│   ├── (auth)/
│   │   ├── login/
│   │   │   ├── page.tsx
│   │   │   └── login-form.tsx
│   │   └── signup/
│   │       ├── page.tsx
│   │       └── signup-form.tsx
│   ├── (protected)/
│   │   ├── dashboard/
│   │   │   ├── page.tsx
│   │   │   └── dashboard-content.tsx
│   │   ├── profile/
│   │   │   ├── page.tsx
│   │   │   └── profile-edit.tsx
│   │   └── settings/
│   │       ├── page.tsx
│   │       └── settings-form.tsx
│   └── components/
│       ├── auth-guard.tsx
│       ├── navigation/
│       │   ├── navbar.tsx
│       │   └── sidebar.tsx
│       └── layout/
│           ├── main-layout.tsx
│           └── auth-layout.tsx
├── hooks/
│   ├── use-auth.ts
│   └── use-protected-route.ts
├── lib/
│   ├── auth/
│   │   ├── auth-service.ts
│   │   └── token-utils.ts
│   └── router/
│       └── route-guard.ts
├── types/
│   ├── auth.ts
│   └── route.ts
└── utils/
    └── route-helpers.ts

Module Boundaries and Import Rules

  • Feature isolation: Each route group ((auth), (protected)) is isolated to prevent cross-feature imports
  • Shared modules: Common components and utilities live in /src/lib and /src/components
  • No circular dependencies: Auth components cannot import from protected routes, and vice versa
  • Barrel exports: /src/components/index.ts exports all shared components for easy consumption

Shared vs Feature-Specific Code Separation

Shared code (src/lib, src/components):

  • Authentication guards and utilities
  • Global layout components (navbar, sidebar)
  • Common UI patterns and design system components

Feature-specific code (src/app/(auth), src/app/(protected)):

  • Route-specific components and pages
  • Feature-level hooks and services
  • Protected route logic and guards

This structure enables:

  1. Clear separation of concerns between auth and protected features
  2. Easy maintenance and scalability of route protection logic
  3. Consistent component reusability across protected routes
  4. Isolated testing and development workflows per feature group

14. Dependencies & Third-Party Libraries

Key Libraries Selected with Justification

React Router DOM v6+

  • Justification: Modern routing solution with built-in support for nested routes, dynamic route matching, and declarative navigation. Provides excellent TypeScript support and ecosystem integration.
  • Bundle impact: ~15KB gzipped
  • Alternatives considered: Next.js Router (not applicable for SPA), Reach Router (less active maintenance)

React Query v5+

  • Justification: Production-ready data fetching library with built-in caching, background updates, and automatic revalidation. Excellent for handling protected route data fetching and server state management.
  • Bundle impact: ~12KB gzipped
  • Alternatives considered: React Apollo (GraphQL-specific), SWR (less mature for complex state scenarios)

Zustand v4+

  • Justification: Lightweight, opinionated state management solution that works well for authentication and session state without the complexity of Redux. Ideal for small to medium-sized applications.
  • Bundle impact: ~2KB gzipped
  • Alternatives considered: Redux Toolkit (more boilerplate), Jotai (more complex for simple use cases)

Axios v1+

  • Justification: Robust HTTP client with request/response interceptors, automatic JSON parsing, and comprehensive error handling. Perfect for centralized auth token management and API communication.
  • Bundle impact: ~10KB gzipped
  • Alternatives considered: Fetch API (more boilerplate), ky (less mature ecosystem)

Styled Components / Tailwind CSS

  • Justification: For styling, Tailwind CSS provides utility-first approach with better developer experience and performance. Styled Components offers component-scoped styling with type safety.
  • Bundle impact: Tailwind ~20KB (with purge), Styled Components ~8KB
  • Alternatives considered: CSS Modules (less developer experience), Emotion (more complex setup)

Alternatives Considered and Rejected

Redux Toolkit

  • Rejected: Too much boilerplate for simple authentication state management. Overkill for this use case where Zustand provides better ergonomics and smaller bundle size.

React Context API + useReducer

  • Rejected: While functional, it lacks the ecosystem support and developer experience of dedicated state management solutions like Zustand or Redux Toolkit.

Next.js App Router

  • Rejected: This is a Single Page Application (SPA) design, not a Next.js application. App Router would introduce unnecessary complexity for client-side route protection.

React Navigation (Native)

  • Rejected: This is a web frontend design, not mobile native. React Navigation is for React Native applications.

Licensing and Bundle Size Impact Assessment

MIT Licensed Libraries: All selected libraries are MIT licensed, providing flexibility for commercial use with minimal legal risk.

Bundle Size Budget Allocation:

  • Core routing and state management: ~30KB (React Router + Zustand + React Query)
  • HTTP client and API handling: ~10KB (Axios)
  • Styling solution: 20KB (Tailwind CSS with purge optimization)
  • Total estimated runtime bundle size: ~60KB gzipped

Tree-shaking Strategy: All libraries support tree-shaking to minimize final bundle size. React Query and Axios are configured to only include necessary modules.

Performance Impact: Bundle size is optimized through:

  1. Tree-shaking for unused imports
  2. Code splitting via React.lazy for route components
  3. Dynamic imports for conditional feature loading
  4. Tailwind CSS purge to remove unused styles

Security Considerations: All libraries are actively maintained with security tracking. Regular updates are scheduled to address vulnerabilities.

Third-Party Integration Points

Authentication Providers:

  • Auth0 / Firebase / Supabase (pluggable authentication strategies)
  • OAuth2 integration through libraries like react-oauth2-provider

Monitoring Tools:

  • Sentry (for error tracking)
  • LogRocket (for session replay)

Analytics Platforms:

  • Google Analytics 4
  • Mixpanel (for user behavior tracking)

15. Open Questions, Risks & Trade-offs

Decisions that need product/engineering alignment

  • Authentication flow granularity: Should route protection be based on user roles, permissions, or just authentication status? This impacts the complexity of our auth middleware and how we model user capabilities in state.
  • Redirect behavior: Should protected routes redirect to login or show a "forbidden" page when authentication fails? This affects UX consistency and error handling strategy.
  • Cache invalidation policy: How aggressively should we invalidate cached protected content when user permissions change? This impacts performance vs. correctness trade-offs.

Known trade-offs made in this design

  • Bundle size vs. flexibility: Using React Router's built-in route protection rather than custom guards increases bundle size but provides better integration with existing router features and reduces maintenance overhead.
  • Client-side vs. server-side validation: Route protection is primarily client-side for UX, but this creates a security risk if not also validated server-side. This trade-off is acceptable for non-sensitive routes but requires careful documentation.
  • User experience vs. security: Redirecting users to login page on protected route access provides better UX than showing error pages, but this can mask potential permission issues that should be handled more explicitly.

Risks and mitigations

  • Race condition in auth state: There's a risk that route guards execute before authentication status is fully resolved, potentially causing incorrect redirects. Mitigation: Implement proper loading states and ensure auth state is ready before route guards execute.
  • Infinite redirect loop: If login page itself requires authentication, it could cause an infinite redirect loop. Mitigation: Explicitly exclude public routes from auth protection and implement proper route exclusion logic.
  • Stale permissions: User permissions may not be updated in real-time, leading to access denied errors after role changes. Mitigation: Implement cache refresh strategies and proper session management.

Technical debt acknowledged

  • Hardcoded route exclusions: Current implementation uses hardcoded public route paths, which creates maintenance overhead as the application grows. Future improvement would involve a more dynamic route registration system.
  • Limited permission granularity: Basic role-based protection doesn't support fine-grained permissions or dynamic permission checks. This could become a bottleneck for complex authorization requirements.
  • No fallback for failed auth: The current design assumes authentication will always resolve successfully. In case of auth failures, there's no graceful fallback mechanism for users to recover from authentication issues.
  • Route guard complexity: As more complex route protection logic is added (e.g., time-based access, feature flags), the guard implementation becomes harder to maintain and test. This creates technical debt in the auth middleware layer.

The design prioritizes developer experience and immediate implementation over long-term extensibility, which may require future refactoring as the application's auth requirements become more sophisticated.

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.

  • Frontend LLD

    Permission-Based UI Rendering System Design

    Comprehensive low-level design for implementing role-based and attribute-based access control in frontend applications with dynamic UI component rendering.

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.