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
- What authentication method will be used (JWT, OAuth, SAML, etc.) and what are the token lifecycle requirements?
- Will this route protection apply to all routes or only specific protected routes, and how are those determined?
- What is the expected behavior when a user accesses a protected route without proper authentication (redirect to login, show unauthorized page, etc.)?
- Do we need to support role-based access control (RBAC) in addition to basic authentication?
- Should the route protection work for both client-side navigation and direct URL access (e.g., copy-paste)?
- Are there specific browser or device support requirements (mobile-first, older browsers like IE11)?
- 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
protectedflag 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 redirectsAuthLayout- Manages authenticated user layout with navigationDashboard- Main authenticated content containerProfile- User profile management containerSettings- Configuration management container
Dumb Components (Presentational Components):
Navbar- Navigation bar with linksLogin- Authentication form componentRegister- Registration form componentHome- Public landing pageNotFound- 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
- ProtectedRoute is highly reusable across different authenticated routes
- AuthLayout provides consistent layout for all authenticated pages
- Navbar is reused across multiple authenticated views
- Login/Register forms are kept as reusable components but can be customized per route
Component Lifecycle Considerations
-
ProtectedRoute lifecycle:
- Mounts and immediately checks authentication
- Handles redirect on auth failure
- Unmounts when route changes
-
AuthLayout lifecycle:
- Mounts with initial auth state
- Handles auth status changes via useEffect
- Cleans up auth listeners on unmount
-
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:
- Update local state optimistically
- Make API call to validate credentials
- If successful, update global auth context
- 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
Navigation Guards and Authentication Flows
// 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:
- Route-level guards:
ProtectedRoutecomponent that checks authentication status - Token validation: Periodic token expiration checks with automatic refresh
- Redirect handling: Preserves return URLs for seamless navigation
- Graceful degradation: Fallback to login page on token refresh failure
Key implementation details:
- Uses React Router v6's
useNavigatefor 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:
- Authentication: Injects auth tokens in headers for protected routes
- Error normalization: Transforms API errors to consistent format
- 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:
srcsetandsizesattributes 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: swapto 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:
useCallbackfor event handlers passed down to child componentsuseMemofor expensive calculations (e.g., filtered lists, computed values)React.memofor presentational components with stable props
What NOT to memoize:
- Simple state updates (e.g.,
useStatesetter 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:
- Application-wide error boundary - Covers entire app for unhandled errors
- Route-level error boundaries - Handle route-specific failures (e.g., data fetch errors)
- Component-level error boundaries - Handle specific UI component failures (e.g., chart rendering errors)
- 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:
- Anonymous user accessing protected route
- Authenticated user navigating between routes
- 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 modalsaria-modal="true"to indicate modal statearia-labelledbyfor accessible labelsaria-describedbyfor form field descriptionsaria-live="polite"for status updatestabindex="-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, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
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:
- Critical Alerts: >50% increase in error rate or performance regressions
- Warning Alerts: >25% increase in performance metrics or user journey drops
- 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/liband/src/components - No circular dependencies: Auth components cannot import from protected routes, and vice versa
- Barrel exports:
/src/components/index.tsexports 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:
- Clear separation of concerns between auth and protected features
- Easy maintenance and scalability of route protection logic
- Consistent component reusability across protected routes
- 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:
- Tree-shaking for unused imports
- Code splitting via React.lazy for route components
- Dynamic imports for conditional feature loading
- 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.