Cross-Site Request Forgery represents one of the most critical security vulnerabilities in web applications, requiring multi-layered protection mechanisms that combine token-based validation, origin checking, and request integrity verification to prevent unauthorized actions on behalf of authenticated users.
1. Problem Statement & Requirements
Clarifying questions
- What's the target browser support matrix? Do we need to support legacy browsers like IE11 or older Edge versions?
- Is this feature part of a larger application that already has authentication and session management in place, or is it a standalone feature?
- Are there any specific real-time requirements for CSRF protection (e.g., live forms, auto-saved drafts)?
- Will this feature support offline scenarios where tokens might expire or need regeneration?
- Do we have any accessibility requirements that might impact how CSRF tokens are handled (e.g., screen reader compatibility)?
- Is there a design system in place that we should integrate with for token display or error handling?
- What's the expected user journey flow? Is this a single-page application with dynamic route changes or a multi-page form?
Functional requirements
- Generate and embed CSRF tokens in forms and API requests
- Validate CSRF tokens server-side for all state-modifying operations (POST, PUT, DELETE)
- Automatically refresh or regenerate tokens when they expire or become invalid
- Support both form-based and AJAX-based API requests with CSRF token inclusion
- Provide fallback mechanisms for token regeneration in case of invalid tokens
- Display user-friendly error messages when CSRF validation fails
- Implement token storage that persists across page refreshes but is cleared on logout
Non-functional requirements
- Security: CSRF tokens must be cryptographically secure and tied to user sessions
- Performance: Token generation and validation should not introduce noticeable latency
- Reliability: Token handling must be resilient to network failures and session timeouts
- Scalability: Token management should scale with concurrent user sessions
- Compatibility: Must work across different frontend frameworks and deployment environments
Constraints and assumptions
- CSRF tokens are generated server-side and must be securely transmitted to the client
- Token validation assumes proper session management and user authentication
- Client-side token storage is assumed to be secure (e.g., HTTP-only cookies or secure local storage)
- The application has a centralized authentication and session management system
- Server-side implementation handles token regeneration and invalidation logic
Out of scope
- Token storage in browser cookies (this is handled by the authentication system)
- Session timeout handling (handled by existing session management)
- Token encryption or obfuscation (assumed to be handled by server-side security)
- Cross-origin request protection (handled by CORS policies and backend validation)
- Token revocation for specific users or sessions (handled by session management system)
- Token expiration time configuration (handled by backend security policies)
2. Component Architecture
graph TD
A[CSRFProtectionProvider] --> B[CSRFTokenManager]
A --> C[AuthContext]
B --> D[CSRFTokenInput]
B --> E[CSRFTokenHeader]
C --> F[ProtectedRoute]
F --> G[ProtectedComponent]
F --> H[AuthRedirect]
Component Tree and Responsibilities
The CSRF protection system is built as a provider pattern that wraps the application and manages token lifecycle. The component architecture includes:
- CSRFProtectionProvider - Root provider that initializes and manages the CSRF token lifecycle
- CSRFTokenManager - Internal component that handles token fetching, storage, and injection
- CSRFTokenInput - Hidden form field component for token inclusion in forms
- CSRFTokenHeader - HTTP header injection component for API requests
- ProtectedRoute - Route guard that ensures CSRF tokens are present before allowing protected routes
- AuthContext - Authentication context that integrates with CSRF protection
TypeScript Props Contracts
// CSRFProtectionProvider props
interface CSRFProtectionProviderProps {
children: React.ReactNode;
tokenUrl?: string;
tokenHeaderName?: string;
tokenStorageKey?: string;
refreshThreshold?: number; // seconds before token expiry to refresh
}
// CSRFTokenManager props
interface CSRFTokenManagerProps {
tokenUrl?: string;
tokenHeaderName?: string;
tokenStorageKey?: string;
refreshThreshold?: number;
}
// CSRFTokenInput props
interface CSRFTokenInputProps {
name?: string;
value?: string;
}
// CSRFTokenHeader props
interface CSRFTokenHeaderProps {
tokenName?: string;
tokenValue?: string;
}
UI State Machine
stateDiagram-v2
[*] --> Idle
Idle --> Fetching : init()
Fetching --> Valid : tokenReceived
Fetching --> Error : fetchFailed
Valid --> Refreshing : refreshRequired()
Refreshing --> Valid : tokenRefreshed
Valid --> Error : tokenExpired
Error --> Fetching : retryFetch()
Component Responsibilities
-
CSRFProtectionProvider:
- Initializes CSRF protection system
- Sets up token refresh intervals
- Manages global error handling for token failures
-
CSRFTokenManager:
- Fetches CSRF tokens from server
- Stores tokens in secure storage (HttpOnly cookies or localStorage)
- Handles token refresh logic
- Manages token lifecycle and expiration
-
CSRFTokenInput:
- Renders hidden form field with CSRF token
- Ensures token is included in form submissions
-
CSRFTokenHeader:
- Injects CSRF token into HTTP headers for API requests
- Handles token rotation and refresh
-
ProtectedRoute:
- Guards protected routes
- Ensures valid CSRF token exists before navigation
- Redirects to login if token invalid
Reusability Decisions
The CSRF protection system is designed as a global provider that can be reused across any application requiring CSRF protection. Individual components like CSRFTokenInput and CSRFTokenHeader are highly reusable and can be composed into any form or API request pattern.
Component Lifecycle Considerations
- Mount: Provider initializes token fetching and refresh intervals
- Unmount: Clears refresh intervals and cleans up event listeners
- Token Refresh: Scheduled polling with exponential backoff for failed refreshes
- Error Handling: Graceful degradation when token fetch fails
State Management Integration
The CSRF protection system integrates with the application's global state management (e.g., Redux or Zustand) to:
- Store token state in a centralized location
- Notify other components of token changes
- Handle authentication state transitions
- Support optimistic updates for token refresh operations
Performance Considerations
- Token fetching is debounced to prevent excessive API calls
- Token refresh is scheduled with exponential backoff
- Hidden input fields are rendered only when needed
- HTTP header injection is optimized to avoid unnecessary DOM updates
Security Considerations in Component Design
- Token storage uses secure HTTP-only cookies when possible
- Token values are never exposed in DOM or console logs
- Form fields are properly sanitized and validated
- Token refresh logic includes proper error handling to prevent token exhaustion
Accessibility Considerations
- Hidden input fields are properly labeled for screen readers
- Token injection does not interfere with keyboard navigation
- Error states provide clear feedback to users without exposing sensitive token information
Testing Strategy
- Unit tests for token fetching and refresh logic
- Integration tests for form submission with CSRF tokens
- E2E tests for protected route access and token expiration handling
- Mock server responses to test various error conditions
Error Handling in Components
- Token fetch failures trigger fallback mechanisms
- Expired tokens automatically attempt refresh
- Invalid token errors redirect to login or show appropriate error messages
- Component-level error boundaries prevent cascading failures
3. State Management Design
Local state vs. global state decisions
For CSRF protection, we need to manage:
- CSRF token storage and lifecycle
- Token validation status
- Request queue for failed requests due to token expiration
- Authentication state that affects token validity
Local state is appropriate for:
- Per-request token validation status
- Temporary request queue management
Global state is required for:
- CSRF token persistence across navigation
- Token refresh coordination across components
- Authentication context that affects all API calls
State management approach
Zustand is selected as the state management solution because:
- Minimal boilerplate with simple API
- Excellent TypeScript support with type inference
- No provider wrapping required - direct hook usage
- Easy to test and mock
- Good performance characteristics with proper memoization
Zustand is preferred over Redux Toolkit for this use case because:
- Less boilerplate code for simple state management
- Easier to integrate with existing React patterns
- Better developer experience for small to medium scale applications
State shape and normalization strategy
interface CsrfState {
token: string | null;
isValid: boolean;
lastRefreshed: number | null;
pendingRequests: PendingRequest[];
isRefreshing: boolean;
}
interface PendingRequest {
id: string;
url: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
body?: any;
timestamp: number;
}
const initialState: CsrfState = {
token: null,
isValid: false,
lastRefreshed: null,
pendingRequests: [],
isRefreshing: false,
};
Server state vs. client state separation
Server State (Managed by backend):
- CSRF token generation and validation
- Token expiration policies
- Session management
Client State (Managed by frontend):
- Current token value and validity status
- Pending requests queue
- Token refresh status
Cache invalidation strategy
Token invalidation occurs when:
- Token expires (based on server-side TTL)
- Server returns 403 Forbidden with CSRF error
- User logs out or session ends
When invalidation occurs:
- Clear current token from state
- Retry pending requests with new token
- Invalidate related cached API responses
Optimistic updates design
Optimistic updates are NOT used for CSRF-protected operations because:
- CSRF tokens are tied to specific sessions and request contexts
- Token validation must be atomic with the actual operation
- Incorrect optimistic updates could lead to security vulnerabilities
Instead, we implement:
- Request queuing for failed requests
- Automatic token refresh and retry mechanism
- Proper error handling with user feedback
Critical-path hook implementation
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import { v4 as uuidv4 } from 'uuid';
interface CsrfState {
token: string | null;
isValid: boolean;
lastRefreshed: number | null;
pendingRequests: PendingRequest[];
isRefreshing: boolean;
}
interface CsrfActions {
setToken: (token: string | null) => void;
validateToken: () => void;
addPendingRequest: (request: Omit<PendingRequest, 'id' | 'timestamp'>) => string;
removePendingRequest: (id: string) => void;
setRefreshing: (isRefreshing: boolean) => void;
refreshAndRetryPending: () => Promise<void>;
}
interface PendingRequest {
id: string;
url: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
body?: any;
timestamp: number;
}
type CsrfStore = CsrfState & CsrfActions;
const useCsrfStore = create<CsrfStore>()(
immer((set, get) => ({
token: null,
isValid: false,
lastRefreshed: null,
pendingRequests: [],
isRefreshing: false,
setToken: (token) => {
set((state) => {
state.token = token;
state.isValid = !!token;
if (token) {
state.lastRefreshed = Date.now();
}
});
},
validateToken: () => {
set((state) => {
state.isValid = !!state.token;
});
},
addPendingRequest: (request) => {
const id = uuidv4();
set((state) => {
state.pendingRequests.push({
...request,
id,
timestamp: Date.now(),
});
});
return id;
},
removePendingRequest: (id) => {
set((state) => {
state.pendingRequests = state.pendingRequests.filter(
(req) => req.id !== id
);
});
},
setRefreshing: (isRefreshing) => {
set((state) => {
state.isRefreshing = isRefreshing;
});
},
refreshAndRetryPending: async () => {
const { token, isRefreshing, pendingRequests } = get();
if (isRefreshing || !token || pendingRequests.length === 0) {
return;
}
set((state) => {
state.isRefreshing = true;
});
try {
// Fetch new token from backend
const newToken = await fetchNewCsrfToken();
set((state) => {
state.token = newToken;
state.isValid = true;
state.lastRefreshed = Date.now();
});
// Retry all pending requests with new token
await retryPendingRequests(pendingRequests, newToken);
} catch (error) {
console.error('Failed to refresh CSRF token:', error);
} finally {
set((state) => {
state.isRefreshing = false;
});
}
},
}))
);
// Custom hook for CSRF-protected API calls
const useCsrfProtectedApi = () => {
const { token, addPendingRequest, removePendingRequest } = useCsrfStore();
const makeCsrfProtectedRequest = async (
url: string,
options?: RequestInit
): Promise<Response> => {
const requestId = addPendingRequest({
url,
method: (options?.method || 'GET') as any,
body: options?.body,
});
try {
const response = await fetch(url, {
...options,
headers: {
...options?.headers,
'X-CSRF-Token': token || '',
},
});
if (response.status === 403) {
const errorData = await response.json();
if (errorData?.code === 'CSRF_TOKEN_INVALID') {
// Token expired, queue retry
await useCsrfStore.getState().refreshAndRetryPending();
}
}
return response;
} finally {
removePendingRequest(requestId);
}
};
return { makeCsrfProtectedRequest };
};
// Helper function to fetch new CSRF token
const fetchNewCsrfToken = async (): Promise<string> => {
const response = await fetch('/api/csrf-token', {
method: 'GET',
credentials: 'include',
});
if (!response.ok) {
throw new Error('Failed to fetch CSRF token');
}
const data = await response.json();
return data.token;
};
// Helper function to retry pending requests
const retryPendingRequests = async (
requests: PendingRequest[],
newToken: string
): Promise<void> => {
const retryPromises = requests.map(async (req) => {
try {
await fetch(req.url, {
method: req.method,
body: req.body,
headers: {
'X-CSRF-Token': newToken,
},
});
} catch (error) {
console.error(`Failed to retry request to ${req.url}:`, error);
}
});
await Promise.allSettled(retryPromises);
};
export { useCsrfStore, useCsrfProtectedApi };
The critical-path hook returns:
token: Current CSRF token valueisValid: Boolean indicating token validity statusaddPendingRequest: Method to queue requests for retryremovePendingRequest: Method to remove completed requests from queuerefreshAndRetryPending: Async method to refresh token and retry queued requests
Key state transitions:
- Token set → isValid becomes true
- Token cleared → isValid becomes false
- Refresh in progress → isRefreshing becomes true
- Refresh complete → isRefreshing becomes false
Side-effect handlers:
- Automatic token refresh on 403 errors
- Request queuing for failed operations
- Token validation lifecycle management
4. Routing & Navigation
Route Structure and Hierarchy
graph TD
A[Root] --> B[Public Routes]
A --> C[Protected Routes]
B --> D[Login]
B --> E[Register]
B --> F[Forgot Password]
C --> G[Dashboard]
C --> H[Profile]
C --> I[Settings]
C --> J[Admin]
The application follows a protected route structure with:
- Public routes: Login, Register, Forgot Password (no authentication required)
- Protected routes: Dashboard, Profile, Settings, Admin (require authentication)
- Nested route structure for admin features and user settings
Code Splitting Strategy
Route-based code splitting with dynamic imports:
// routes.tsx
const Login = React.lazy(() => import('pages/Login'));
const Dashboard = React.lazy(() => import('pages/Dashboard'));
const Profile = React.lazy(() => import('pages/Profile'));
// Protected route wrapper
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { isAuthenticated, loading } = useAuth();
if (loading) return <LoadingSpinner />;
if (!isAuthenticated) return <Navigate to="/login" />;
return children;
};
// Route configuration with lazy loading
const routes = [
{
path: '/login',
element: <Login />,
lazy: true
},
{
path: '/dashboard',
element: (
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
),
lazy: true
}
];
Justification: Route-based splitting ensures that protected routes are only loaded when needed, reducing initial bundle size. Public pages load immediately for fast first-paint.
Navigation Guards and Authentication Flows
// auth-guard.ts
export const AuthGuard: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { isAuthenticated, loading, refreshToken } = useAuth();
const navigate = useNavigate();
useEffect(() => {
if (loading) return;
if (!isAuthenticated) {
// Store intended destination
const redirectUrl = window.location.pathname + window.location.search;
localStorage.setItem('redirectUrl', redirectUrl);
navigate('/login');
} else {
// Check token validity and refresh if needed
refreshToken();
}
}, [isAuthenticated, loading, navigate]);
return loading ? <LoadingSpinner /> : children;
};
Authentication flow:
- User accesses protected route without valid session
- Redirect to login page with intended URL stored in localStorage
- After successful login, redirect to stored URL or default dashboard
- Token refresh logic handles session expiration gracefully
Deep Linking Support and URL Parameter Design
// URL structure examples:
// /dashboard?tab=analytics&filter=date-range
// /profile/settings?section=notifications&theme=dark
// /admin/users?status=active&page=2
// URL parameter validation and sanitization
const validateAndSanitizeParams = (params: Record<string, string>) => {
const sanitized = {};
Object.entries(params).forEach(([key, value]) => {
// Validate parameter names
if (!/^[a-zA-Z0-9_-]+$/.test(key)) {
throw new Error(`Invalid parameter name: ${key}`);
}
// Sanitize values
sanitized[key] = sanitizeString(value);
});
return sanitized;
};
Browser History Management and Scroll Restoration
// scroll-restoration.ts
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
export const ScrollToTop: React.FC = () => {
const location = useLocation();
useEffect(() => {
// Scroll to top on route change
window.scrollTo(0, 0);
// Restore scroll position for cached routes (if supported)
const scrollPosition = sessionStorage.getItem(`scrollPosition-${location.pathname}`);
if (scrollPosition) {
window.scrollTo(0, parseInt(scrollPosition));
}
}, [location.pathname]);
// Save scroll position before navigation
useEffect(() => {
const handleBeforeUnload = () => {
sessionStorage.setItem(
`scrollPosition-${location.pathname}`,
window.scrollY.toString()
);
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [location.pathname]);
return null;
};
// App.tsx
function App() {
return (
<BrowserRouter>
<ScrollToTop />
<Routes>
{/* ... routes */}
</Routes>
</BrowserRouter>
);
}
Scroll restoration strategy:
- Default to top scroll on route changes
- Session storage for preserving scroll positions across navigation
- BeforeUnload event handler to capture current scroll position
- Optimized for SPA navigation patterns with minimal performance impact
5. API Integration Layer
API Client Design Pattern
The frontend will implement a centralized API client using a fetch wrapper pattern with Axios as the underlying HTTP client. This approach provides:
- Centralized request/response handling
- Built-in interceptors for authentication and error handling
- Type-safe API endpoints with TypeScript integration
- Consistent error response formatting across all API calls
// api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000';
const createApiClient = (): AxiosInstance => {
const client = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
withCredentials: true, // Required for CSRF cookie handling
});
return client;
};
export const apiClient = createApiClient();
Request/Response Interceptors
Request interceptors handle CSRF token injection and authentication headers, while response interceptors manage error responses and token refresh logic.
// api/interceptors.ts
import { apiClient } from './client';
import { getCSRFToken, setCSRFToken } from '@/utils/csrf';
// Request interceptor - inject CSRF token and auth headers
apiClient.interceptors.request.use(
async (config) => {
const csrfToken = getCSRFToken();
if (csrfToken) {
config.headers['X-CSRF-Token'] = csrfToken;
}
const authHeader = getAuthHeader();
if (authHeader) {
config.headers['Authorization'] = authHeader;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor - handle CSRF token refresh and error responses
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// Handle CSRF token expiration
if (error.response?.status === 403 && error.response.data?.code === 'CSRF_TOKEN_EXPIRED') {
// Refresh CSRF token from server
try {
const { data } = await apiClient.get('/csrf-token');
setCSRFToken(data.token);
// Retry original request with new token
originalRequest.headers['X-CSRF-Token'] = data.token;
return apiClient(originalRequest);
} catch (refreshError) {
// If refresh fails, redirect to login
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
// Handle authentication errors
if (error.response?.status === 401) {
// Clear auth state and redirect to login
clearAuthState();
window.location.href = '/login';
}
return Promise.reject(error);
}
);
// Helper functions
const getAuthHeader = (): string | null => {
const token = localStorage.getItem('authToken');
return token ? `Bearer ${token}` : null;
};
const clearAuthState = () => {
localStorage.removeItem('authToken');
sessionStorage.removeItem('authToken');
};
Error Handling and Retry Strategy
Comprehensive error handling includes:
- HTTP status code categorization (client vs server errors)
- Retry logic for transient network issues
- User-friendly error messages with fallbacks
- Detailed logging for debugging
// api/errorHandler.ts
import { AxiosError } from 'axios';
export interface ApiError {
status: number;
message: string;
code?: string;
details?: any;
}
export const handleApiError = (error: AxiosError): ApiError => {
if (error.response) {
// Server responded with error status
const { status, data } = error.response;
return {
status,
message: data?.message || `Server Error (${status})`,
code: data?.code,
details: data?.details
};
} else if (error.request) {
// Request was made but no response received
return {
status: 0,
message: 'Network Error - Unable to reach server',
code: 'NETWORK_ERROR'
};
} else {
// Something else happened in setting up the request
return {
status: 0,
message: error.message || 'Unknown Error',
code: 'UNKNOWN_ERROR'
};
}
};
// Retry configuration
export const retryConfig = {
retries: 3,
retryDelay: (retryCount: number) => Math.pow(2, retryCount) * 1000,
retryOn: [500, 502, 503, 504, 408]
};
Loading, Error, and Empty States Design
The API integration layer provides consistent state management for all data operations:
// api/types.ts
export interface ApiResult<T> {
data: T | null;
loading: boolean;
error: ApiError | null;
refetch: () => void;
}
export interface ApiResponse<T> {
data: T;
metadata?: {
total?: number;
page?: number;
pageSize?: number;
};
}
Data Fetching Strategy
For this feature, we'll use client-side rendering (CSR) with React Query for optimal state management:
// hooks/useApi.ts
import { useQuery, useMutation, QueryClient } from '@tanstack/react-query';
import { apiClient } from '@/api/client';
// Query client configuration
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
retry: 2,
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
},
},
});
// Generic API query hook
export const useApiQuery = <T>(
key: string,
endpoint: string,
options?: any
) => {
return useQuery<T>({
queryKey: [key, endpoint],
queryFn: async () => {
const response = await apiClient.get<T>(endpoint);
return response.data;
},
...options,
});
};
// Generic API mutation hook
export const useApiMutation = <T, Variables = any>(
mutationKey: string,
endpoint: string,
options?: any
) => {
return useMutation<T, AxiosError, Variables>({
mutationKey: [mutationKey, endpoint],
mutationFn: async (variables) => {
const response = await apiClient.post<T>(endpoint, variables);
return response.data;
},
...options,
});
};
API Response Transformation Layer
All API responses are normalized through a consistent transformation layer:
// api/transformers.ts
export interface User {
id: string;
name: string;
email: string;
role: string;
}
export interface UserResponse {
id: string;
name: string;
email: string;
role: string;
}
export const transformUserResponse = (response: UserResponse): User => ({
id: response.id,
name: response.name,
email: response.email,
role: response.role,
});
// Generic transformer factory
export const createTransformer = <T, U>(transformer: (input: T) => U) => {
return (data: T): U => transformer(data);
};
Sequence Diagram for Primary Data Flows
sequenceDiagram
participant Client
participant AuthServer
participant ApiServer
Note over Client,ApiServer: CSRF Token Acquisition
Client->>AuthServer: GET /csrf-token
AuthServer-->>Client: CSRF Token Cookie + Header
Note over Client,ApiServer: Protected API Request
Client->>ApiServer: POST /api/users (with X-CSRF-Token)
ApiServer->>ApiServer: Validate CSRF Token
ApiServer-->>Client: 201 Created / 403 Forbidden
Note over Client,ApiServer: Token Refresh
Client->>AuthServer: GET /csrf-token (expired)
AuthServer-->>Client: New CSRF Token Cookie + Header
Client->>ApiServer: Retry POST with new token
ApiServer-->>Client: 201 Created
CSRF Token Management Strategy
The system implements a robust CSRF token lifecycle management:
- Initial Token Acquisition: On login or page load, fetch CSRF token from server
- Token Storage: Store in HTTP-only cookie for security and in-memory for client access
- Token Validation: Include token in all state-changing requests
- Token Refresh: Automatically refresh expired tokens with minimal user interruption
// utils/csrf.ts
export const CSRF_COOKIE_NAME = 'csrf_token';
export const getCSRFToken = (): string | null => {
// Try to get from cookie first (more secure)
const cookieValue = document.cookie
.split('; ')
.find(row => row.startsWith(`${CSRF_COOKIE_NAME}=`));
if (cookieValue) {
return cookieValue.split('=')[1];
}
// Fallback to localStorage for client-side access
return localStorage.getItem('csrf_token');
};
export const setCSRFToken = (token: string): void => {
localStorage.setItem('csrf_token', token);
// Set cookie with secure flags (in production)
if (typeof window !== 'undefined') {
document.cookie = `${CSRF_COOKIE_NAME}=${token}; path=/; secure; samesite=strict`;
}
};
export const clearCSRFToken = (): void => {
localStorage.removeItem('csrf_token');
document.cookie = `${CSRF_COOKIE_NAME}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
};
6. Performance Design
Core Web Vitals Targets
- Largest Contentful Paint (LCP): <2.5s for 95% of users
- Interaction to Next Paint (INP): <200ms for 90% of interactions
- Cumulative Layout Shift (CLS): <0.1 for 95% of users
- First Contentful Paint (FCP): <1.8s for 95% of users
Bundle Size Budget per Route
- Landing Page: <200KB (including all dependencies)
- Dashboard Route: <400KB (with charts, tables, and widgets)
- Settings Route: <150KB (form components, validation)
- Profile Route: <100KB (avatar, basic info display)
Critical Rendering Path Optimization
- HTML: Inline critical CSS and defer non-critical resources
- CSS: Use CSS-in-JS with selective extraction for critical styles
- JS: Split bundles by route and lazy-load non-critical components
- Fonts: Preload critical font subsets with
font-display: swap
Image Optimization Strategy
- Formats: Use WebP for modern browsers, JPEG fallback for older
- Lazy Loading: Implement
loading="lazy"withIntersectionObserver - Responsive Images: Use
srcSetandsizesattributes for device-specific images - Compression: Apply lossless compression for icons, lossy for content images
Font Loading Strategy
- Preload Critical Fonts:
<link rel="preload" as="font">for body text - Font Display:
font-display: swapto prevent FOIT - Subset Loading: Load only necessary character sets for each locale
Memoization Strategy
- React.memo: Apply to presentational components with stable props
- useCallback: Wrap event handlers and function factories in containers
- useMemo: Cache expensive computations or derived state values (e.g., filtered lists, computed totals)
- Avoid Over-Memoization: Don't memoize simple props like strings or numbers
Virtualization for Large Lists
- React Window: Implement virtual scrolling for lists > 100 items
- Dynamic Item Size: Use
FixedSizeListfor uniform items,VariableSizeListfor variable heights - Windowing Strategy: Render only visible items plus a buffer of 5–10 items
Prefetching and Preloading Strategy
- Route-based Preload: Use
preloadattribute for critical route dependencies (e.g., dashboard data) - Link Prefetching: Implement
rel="prefetch"for likely next-page resources (e.g., user profile avatar) - Data Prefetching: Use React Query's
prefetchQueryfor frequently accessed data (e.g., user settings, dashboard metrics) - Service Worker: Implement caching strategy for static assets and API responses
Performance Monitoring
- Lighthouse CI: Integrate automated performance checks in CI pipeline
- Web Vitals Dashboard: Track real-user performance metrics via Google Analytics 4 or custom RUM
- Bundle Analysis: Use
source-map-explorerandwebpack-bundle-analyzerto identify oversized dependencies
Cache Invalidation Strategy
- ETag-based: Use ETags for API responses to avoid re-fetching unchanged data
- Cache-Control Headers: Implement
max-ageandstale-while-revalidatefor optimal caching - Client-side Cache: Use React Query's cache invalidation patterns to update stale data
Optimistic Updates Design
- Immediate UI Feedback: Update UI immediately on user action (e.g., form submission)
- Revert on Error: Rollback optimistic updates when server response fails
- Loading States: Show loading indicators during optimistic update lifecycle
Critical-Path Hook Implementation
// src/hooks/useOptimisticUpdate.ts
import { useCallback, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
interface OptimisticUpdateConfig<T> {
queryKey: string[];
updateFn: (data: T) => Promise<T>;
rollback?: (originalData: T) => void;
}
export function useOptimisticUpdate<T>({
queryKey,
updateFn,
rollback
}: OptimisticUpdateConfig<T>) {
const [optimisticData, setOptimisticData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState(false);
const queryClient = useQueryClient();
const handleUpdate = useCallback(
async (newData: T) => {
setIsLoading(true);
setOptimisticData(newData);
try {
const result = await updateFn(newData);
queryClient.setQueryData(queryKey, result);
setOptimisticData(null);
} catch (error) {
rollback?.(optimisticData as T);
setOptimisticData(null);
throw error;
} finally {
setIsLoading(false);
}
},
[updateFn, queryKey, rollback, optimisticData]
);
return {
handleUpdate,
isLoading,
optimisticData
};
}
Performance Budget Implementation
// webpack.config.js - Performance budget configuration
module.exports = {
performance: {
maxAssetSize: 250000,
maxEntrypointSize: 300000,
hints: 'error'
}
};
Bundle Analysis Strategy
- Build-time Analysis: Run
webpack-bundle-analyzerafter each build to identify large dependencies - Code Splitting: Implement route-based code splitting using React.lazy and Suspense
- Tree Shaking: Ensure all libraries support ES modules for dead code elimination
Memory Management
- Cleanup Side Effects: Implement proper cleanup in useEffect hooks to prevent memory leaks
- Event Listener Management: Use
useCallbackand proper cleanup in event handlers - Large Data Handling: Implement pagination or virtualization for large datasets to prevent memory bloat
Resource Loading Prioritization
- Critical Path Resources: Load essential JS/CSS in
<head>for fastest render - Non-Critical Resources: Defer loading of analytics, third-party widgets, and non-essential features
- Resource Hints: Use
rel="preload"for critical resources,rel="prefetch"for likely future resources
Accessibility Performance Considerations
- Keyboard Navigation: Ensure all performance optimizations don't break keyboard-only users
- Screen Reader Compatibility: Verify that virtualized lists and lazy-loaded content work with screen readers
- Focus Management: Maintain proper focus order even when components are dynamically loaded or replaced
Monitoring and Alerting
- Real User Monitoring: Track performance metrics in production with error boundaries and logging
- Threshold-based Alerts: Configure alerts for LCP > 3s, CLS > 0.25, INP > 500ms
- Performance Regression Detection: Use automated tools to detect performance regressions in CI/CD pipeline
7. Error Handling & Resilience
Error Boundary Placement Strategy
Error boundaries should be strategically placed to prevent cascading failures while maintaining user experience. The placement follows a hierarchical approach:
- Application-level error boundary - Covers entire app to catch unexpected runtime errors
- Feature-specific boundaries - Wrap major feature components (e.g., dashboard, forms)
- Component-level boundaries - For critical sub-components that might fail independently
graph TD
A[App] --> B[ErrorBoundary]
B --> C[Dashboard]
B --> D[UserForm]
B --> E[Navigation]
C --> F[DashboardGrid]
C --> G[AnalyticsChart]
D --> H[FormInputs]
D --> I[SubmitButton]
Global Error Handler Design
A centralized error handler manages uncaught exceptions and provides consistent feedback:
// src/core/error/GlobalErrorHandler.tsx
import React from 'react';
interface GlobalErrorState {
hasError: boolean;
error?: Error;
errorInfo?: React.ErrorInfo;
}
class GlobalErrorHandler extends React.Component<{}, GlobalErrorState> {
constructor(props: {}) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): GlobalErrorState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Log to monitoring service
console.error('Global Error:', error, errorInfo);
// Send to error tracking service (e.g., Sentry)
if (window.Sentry) {
window.Sentry.captureException(error, {
contexts: { react: errorInfo }
});
}
}
render() {
if (this.state.hasError) {
return (
<div className="error-container">
<h1>Something went wrong.</h1>
<p>We're working to fix this issue.</p>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
export default GlobalErrorHandler;
Offline / Network Failure Handling
Offline resilience is achieved through:
- Network status monitoring using
navigator.onLineAPI - Retry mechanisms with exponential backoff for transient failures
- Offline queue for operations that can be retried later
// src/core/network/NetworkManager.ts
class NetworkManager {
private isOnline: boolean = navigator.onLine;
private offlineQueue: QueueItem[] = [];
constructor() {
window.addEventListener('online', this.handleOnline);
window.addEventListener('offline', this.handleOffline);
}
private handleOnline = () => {
this.isOnline = true;
this.processOfflineQueue();
};
private handleOffline = () => {
this.isOnline = false;
};
async withRetry<T>(
operation: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
let lastError: Error;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
if (attempt === maxRetries || !this.isOnline) {
throw lastError;
}
// Exponential backoff with jitter
const delay = baseDelay * Math.pow(2, attempt) +
Math.random() * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
queueOperation(operation: QueueItem) {
if (!this.isOnline) {
this.offlineQueue.push(operation);
return true;
}
return false;
}
private async processOfflineQueue() {
while (this.offlineQueue.length > 0) {
const operation = this.offlineQueue.shift();
try {
await operation.execute();
} catch (error) {
// Re-queue failed operations
this.offlineQueue.push(operation);
}
}
}
}
export const networkManager = new NetworkManager();
Graceful Degradation Patterns
When features fail, provide fallback experiences:
// src/components/AnalyticsChart.tsx
interface ChartProps {
data?: ChartData[];
loading?: boolean;
error?: Error | null;
}
const AnalyticsChart: React.FC<ChartProps> = ({
data,
loading,
error
}) => {
if (loading) {
return <div className="skeleton-chart">Loading chart...</div>;
}
if (error) {
return (
<div className="chart-fallback">
<p>Chart unavailable due to: {error.message}</p>
<button onClick={() => window.location.reload()}>
Retry
</button>
</div>
);
}
if (!data || data.length === 0) {
return (
<div className="chart-empty">
<p>No data available</p>
</div>
);
}
return (
<div className="chart-container">
{/* Chart rendering logic */}
</div>
);
};
User Feedback on Errors
Consistent error communication patterns:
- Toast notifications for non-blocking errors
- Inline error messages for form validation
- Fallback UIs for critical component failures
// src/core/toast/ToastManager.tsx
interface Toast {
id: string;
message: string;
type: 'success' | 'error' | 'warning' | 'info';
duration?: number;
}
class ToastManager {
private toasts: Toast[] = [];
private listeners: Array<(toasts: Toast[]) => void> = [];
addToast(message: string, type: Toast['type'] = 'info', duration = 5000) {
const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const toast: Toast = {
id,
message,
type,
duration
};
this.toasts.push(toast);
this.notifyListeners();
// Auto-remove toast
if (duration > 0) {
setTimeout(() => this.removeToast(id), duration);
}
}
removeToast(id: string) {
this.toasts = this.toasts.filter(toast => toast.id !== id);
this.notifyListeners();
}
private notifyListeners() {
this.listeners.forEach(listener => listener(this.toasts));
}
subscribe(listener: (toasts: Toast[]) => void) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
}
export const toastManager = new ToastManager();
Error Recovery Strategies
Implement structured recovery paths:
- Automatic retry for transient network issues
- User-initiated refresh for recoverable errors
- Offline mode for persistent failures
- Error logging with context for debugging
// src/core/error/ErrorRecovery.ts
class ErrorRecovery {
static async handleApiError(error: any, context: string) {
const errorInfo = {
timestamp: new Date().toISOString(),
context,
userAgent: navigator.userAgent,
url: window.location.href,
error: error.message || String(error)
};
// Log to monitoring service
if (window.Sentry) {
window.Sentry.captureException(error, {
contexts: { errorInfo }
});
}
// Handle specific error types
if (error.response?.status === 401) {
// Redirect to login for authentication errors
window.location.href = '/login';
return;
}
if (error.response?.status === 403) {
// Show permission error
toastManager.addToast(
'You do not have permission to perform this action',
'error'
);
return;
}
// Show generic error for other cases
toastManager.addToast(
'An unexpected error occurred. Please try again.',
'error'
);
}
}
8. Testing Strategy
Unit Testing
- Component tests: 90%+ coverage for all presentational and container components using React Testing Library
- Custom hook tests: 100% coverage for all custom hooks including error states, loading states, and side-effect execution
- Utility function tests: 100% coverage for all security-related utilities (token generation, validation, sanitization)
- State management tests: Test reducer logic and async action handling in isolation
- Target coverage: 85%+ line coverage, with focus on security boundary conditions and error paths
Integration Testing Strategy
- API integration tests: Validate token flow between frontend and backend services (CSRF token exchange, validation)
- Form submission integration: End-to-end flow testing for forms that require CSRF tokens
- Authentication boundary tests: Verify token invalidation on session changes and logout flows
- Cross-component interaction: Test token propagation between parent-child components and context providers
E2E Testing Scope and Tool
- Tool: Playwright (chosen for better cross-browser support and network interception capabilities)
- Covered flows:
- User login → CSRF token generation → form submission
- Token expiration handling and refresh flow
- Session hijacking prevention (token invalidation on session changes)
- Multi-tab browser behavior and token synchronization
- Network interception: Test token injection in request headers and validation in response handling
Visual Regression Testing
- Tool: Chromatic (with Playwright integration for automated screenshot capture)
- Test cases:
- Token mismatch error UI rendering
- Session timeout error states
- Form submission failure states with proper error messages
- Viewport coverage: Desktop, tablet, mobile breakpoints
Accessibility Testing Automation
- Tool: axe-core with React Testing Library integration
- Tests:
- Token error messages must have proper ARIA labels and keyboard focus
- Form elements must maintain accessible names when CSRF tokens are injected
- Error states must be announced by screen readers
- Coverage: 100% of interactive components with accessibility testing
Test Data Strategy and Fixtures
- Security test fixtures:
- Valid CSRF tokens with different expiration times (immediate, 5min, 1hr)
- Invalid token formats (malformed, expired, mismatched)
- Token tampering scenarios (man-in-the-middle attack simulations)
- Test data isolation: Use unique token values per test run to prevent cross-test contamination
- Mock server setup: Use MSW (Mock Service Worker) for realistic API response simulation with token validation logic
- Environment-specific fixtures: Separate test data for local dev, staging, and production-like environments
9. Accessibility (A11y)
WCAG 2.1 AA compliance checklist for this feature
The CSRF protection implementation must maintain full accessibility compliance:
- Keyboard Navigation: All form elements and actions must be fully operable via keyboard (Tab, Enter, Space)
- Focus Management: Proper focus indicators and trapping for modals/dialogs
- Screen Reader Support: ARIA labels and roles for dynamic content updates
- Contrast Ratios: Text must meet 4.5:1 contrast ratio for normal text and 3:1 for large text
- Error Handling: Clear error messages with proper ARIA states (aria-invalid, aria-describedby)
- Form Labels: All form fields must have accessible labels associated with them
- Skip Links: Keyboard-accessible skip links to bypass repetitive navigation
Keyboard navigation support
All CSRF-protected forms and actions must support complete keyboard navigation:
// Keyboard navigation handler for CSRF-protected forms
const handleKeyboardNavigation = (event: React.KeyboardEvent) => {
if (event.key === 'Enter' && event.target instanceof HTMLButtonElement) {
event.preventDefault();
handleSubmitWithCSRF(event);
}
if (event.key === 'Tab') {
// Handle focus trapping in modals
handleFocusTrap(event);
}
};
Focus management strategy
Proper focus handling for modals and dynamic content:
// Focus trap implementation for CSRF confirmation dialogs
const useFocusTrap = (isOpen: boolean, modalRef: React.RefObject<HTMLDivElement>) => {
useEffect(() => {
if (!isOpen) return;
const focusableElements = modalRef.current?.querySelectorAll(
'button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
) || [];
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
const handleFocus = (event: FocusEvent) => {
if (!modalRef.current?.contains(event.target as Node)) {
lastElement.focus();
}
};
firstElement?.focus();
return () => {
// Restore focus to previous element
document.activeElement?.blur();
};
}, [isOpen]);
};
ARIA roles and attributes plan
// CSRF error state with proper ARIA attributes
const CSRFErrorDisplay = ({ message }: { message: string }) => (
<div
role="alert"
aria-live="assertive"
aria-atomic="true"
className="csrf-error-container"
>
<span
role="status"
aria-label={`CSRF protection error: ${message}`}
className="csrf-error-message"
>
{message}
</span>
</div>
);
// CSRF token input with proper labeling
const CSRFTokenInput = ({ token }: { token: string }) => (
<input
type="hidden"
name="csrf_token"
value={token}
aria-label="CSRF protection token"
aria-describedby="csrf-token-hint"
/>
);
Screen reader compatibility
All dynamic CSRF state changes must be announced to screen readers:
// Screen reader announcement for CSRF events
const ScreenReaderAnnouncer = ({ message }: { message: string }) => (
<div
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{message}
</div>
);
// Example usage in CSRF validation flow
const announceCSRFStatus = (status: 'valid' | 'invalid' | 'expired') => {
const messages = {
valid: 'CSRF validation successful',
invalid: 'CSRF validation failed, please refresh the page',
expired: 'CSRF token has expired, please refresh the page'
};
return <ScreenReaderAnnouncer message={messages[status]} />;
};
Focus restoration patterns
// Focus restoration for CSRF-protected actions
const useCSRFFocusRestore = (action: string) => {
const previousActiveElement = useRef<HTMLElement | null>(null);
useEffect(() => {
// Store current focus before CSRF action
previousActiveElement.current = document.activeElement as HTMLElement;
return () => {
// Restore focus after action completes
if (previousActiveElement.current) {
previousActiveElement.current.focus();
}
};
}, [action]);
};
Accessibility testing automation
All accessibility checks must be automated:
// Accessibility test assertions for CSRF forms
describe('CSRF Form Accessibility', () => {
it('should have proper focus management', async () => {
await render(<CSRFProtectedForm />);
// Verify all form elements are keyboard accessible
const inputs = await screen.getAllByRole('textbox');
expect(inputs).toHaveLength(2);
// Verify proper ARIA attributes
const tokenInput = await screen.getByRole('textbox', { name: 'CSRF token' });
expect(tokenInput).toHaveAttribute('aria-label');
});
it('should announce CSRF errors to screen readers', async () => {
const { container } = render(<CSRFErrorDisplay message="Invalid token" />);
const srAnnouncer = container.querySelector('[aria-live="polite"]');
expect(srAnnouncer).toBeInTheDocument();
const errorText = await screen.findByText('Invalid token');
expect(errorText).toHaveAttribute('aria-live', 'assertive');
});
});
Keyboard navigation testing
// Test case for keyboard navigation in CSRF-protected forms
describe('CSRF Form Keyboard Navigation', () => {
it('should allow tab navigation through form fields', async () => {
const { container } = render(<CSRFProtectedForm />);
const firstInput = await screen.getByLabelText('Username');
const secondInput = await screen.getByLabelText('Password');
// Simulate tab navigation
firstInput.focus();
await userEvent.tab();
expect(secondInput).toHaveFocus();
});
it('should submit form with Enter key', async () => {
const { container } = render(<CSRFProtectedForm />);
const submitButton = await screen.getByRole('button', { name: 'Submit' });
// Simulate Enter key press
await userEvent.type(submitButton, '{enter}');
// Verify form submission event was triggered
expect(mockSubmitHandler).toHaveBeenCalled();
});
});
10. Security Considerations
XSS Prevention Strategy
Content Sanitization: All user-generated content must be sanitized before rendering to prevent script injection. Implement a robust HTML sanitizer like DOMPurify that strips dangerous tags and attributes while preserving safe formatting.
// Example implementation using DOMPurify
import DOMPurify from 'dompurify';
const sanitizeHTML = (rawHTML: string): string => {
return DOMPurify.sanitize(rawHTML, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
ALLOWED_ATTR: ['class'],
});
};
Input Validation: All form inputs must be validated on both client and server side with strict regex patterns and length constraints. Never trust user input.
Template Literal Escaping: When dynamically generating HTML content, ensure proper escaping of template literals to prevent injection attacks.
CSRF Protection
Token Generation & Management: Implement a robust CSRF token system that:
- Generates unique tokens per user session
- Regenerates tokens on sensitive operations (form submissions, API calls)
- Stores tokens in HTTP-only cookies for maximum security
- Includes tokens in all state-changing requests (POST, PUT, DELETE)
// CSRF token management hook
import { useEffect, useState } from 'react';
import { getCSRFToken } from '../api/csrf';
export const useCSRFProtection = () => {
const [csrfToken, setCsrfToken] = useState<string | null>(null);
useEffect(() => {
const fetchCSRFToken = async () => {
try {
const token = await getCSRFToken();
setCsrfToken(token);
} catch (error) {
console.error('Failed to fetch CSRF token:', error);
}
};
fetchCSRFToken();
}, []);
const getCSRFHeaders = () => ({
'X-CSRF-Token': csrfToken || '',
'X-Requested-With': 'XMLHttpRequest'
});
return { csrfToken, getCSRFHeaders };
};
Double Submit Cookie Pattern: Implement double-submit cookie pattern where:
- Server sets a CSRF cookie with a random token value
- Client includes the same token in request headers or form fields
- Server validates that both values match before processing
Content Security Policy (CSP) Headers
Header Implementation: Configure strict CSP headers to prevent XSS and other injection attacks:
// CSP header configuration in server middleware
const cspHeaders = {
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';",
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block'
};
CSP Policy Components:
default-src 'self'- Only allow resources from same originscript-src- Allow inline scripts only for critical functionality, block evalstyle-src- Block unsafe inline styles to prevent CSS injectionimg-src- Restrict image sources to prevent tracking
Sensitive Data Handling in Frontend State
Secure Storage: Never store sensitive data like authentication tokens or session IDs in:
- LocalStorage (vulnerable to XSS)
- SessionStorage (vulnerable to XSS)
- React state without proper sanitization
Secure Token Storage: Use HTTP-only cookies for session tokens and ensure they're:
- Secure (HTTPS only)
- HttpOnly (prevents JavaScript access)
- SameSite (prevents CSRF attacks)
// Secure token handling in React context
interface AuthContextType {
accessToken: string | null;
refreshToken: string | null;
setTokens: (access: string, refresh: string) => void;
clearTokens: () => void;
}
const AuthContext = createContext<AuthContextType>({
accessToken: null,
refreshToken: null,
setTokens: () => {},
clearTokens: () => {}
});
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const [accessToken, setAccessToken] = useState<string | null>(null);
const [refreshToken, setRefreshToken] = useState<string | null>(null);
// Token setting logic that ensures secure storage
const setTokens = (access: string, refresh: string) => {
// Store in secure HTTP-only cookie instead of React state
// This is a simplified example - actual implementation would use server-side storage
setAccessToken(access);
setRefreshToken(refresh);
};
const clearTokens = () => {
setAccessToken(null);
setRefreshToken(null);
};
return (
<AuthContext.Provider value={{ accessToken, refreshToken, setTokens, clearTokens }}>
{children}
</AuthContext.Provider>
);
};
Input Sanitization Approach
Form Validation: Implement comprehensive input validation:
- Client-side: Real-time validation with clear error messages
- Server-side: Complete re-validation before processing
- Rate limiting on form submissions to prevent abuse
API Request Sanitization: All API requests must:
- Validate request method and headers
- Sanitize all URL parameters and query strings
- Implement proper content-type checking
- Validate request payload structure
Security Headers Implementation
Header Configuration: Implement security headers at the server level:
// Express middleware for security headers
const securityHeaders = (req: Request, res: Response, next: NextFunction) => {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
};
Security Headers Benefits:
Strict-Transport-Security: Forces HTTPS connectionsX-Content-Type-Options: Prevents MIME type sniffingX-Frame-Options: Protects against clickjacking attacksReferrer-Policy: Controls referrer information sent to external sites
Security Testing Considerations
Vulnerability Scanning: Integrate automated security scanning tools:
- SAST (Static Application Security Testing) for code analysis
- DAST (Dynamic Application Security Testing) for runtime vulnerability detection
- Dependency scanning for known vulnerabilities in third-party packages
Security Review Process: Establish a security review process that includes:
- Code reviews with security focus
- Penetration testing of critical endpoints
- Regular security audits of authentication flows
This comprehensive security approach ensures that the frontend application is resilient against common web vulnerabilities while maintaining a secure user experience. The design prioritizes defense in depth, implementing multiple layers of protection to safeguard against both client-side and server-side threats.
11. Internationalization (i18n)
Translation Key Structure and Naming Convention
Following BEM-style naming with hierarchical organization:
// Translation keys organized by feature and context
{
"common": {
"buttons": {
"save": "Save",
"cancel": "Cancel",
"submit": "Submit"
},
"labels": {
"username": "Username",
"email": "Email"
}
},
"auth": {
"login": {
"title": "Login to Your Account",
"form": {
"username_label": "Username or Email",
"password_label": "Password",
"remember_me": "Remember me"
},
"errors": {
"invalid_credentials": "Invalid username or password",
"account_locked": "Account locked due to too many failed attempts"
}
}
},
"dashboard": {
"welcome_message": "Welcome back, {{name}}!",
"stats": {
"total_users": "Total Users: {{count}}",
"revenue": "Revenue: {{amount}}"
}
}
}
RTL Support Considerations
RTL layout handling through CSS classes and direction properties:
/* CSS for RTL support */
[dir="rtl"] .text-left {
text-align: right;
}
[dir="rtl"] .float-start {
float: right;
}
/* Component-level RTL handling */
.rtl-container {
direction: rtl;
text-align: right;
}
Date, Number, Currency Formatting Strategy
Using Intl API with fallbacks:
// i18n formatting utilities
export const formatCurrency = (amount: number, locale: string, currency: string) => {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
minimumFractionDigits: 2
}).format(amount);
};
export const formatDate = (date: Date, locale: string, options?: Intl.DateTimeFormatOptions) => {
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
...options
}).format(date);
};
export const formatNumber = (num: number, locale: string, options?: Intl.NumberFormatOptions) => {
return new Intl.NumberFormat(locale, {
maximumFractionDigits: 2,
...options
}).format(num);
};
Locale Detection and Switching Strategy
Automatic detection with manual override:
// i18n context implementation
interface I18nContextType {
locale: string;
setLocale: (locale: string) => void;
t: (key: string, params?: Record<string, unknown>) => string;
formatDate: (date: Date, options?: Intl.DateTimeFormatOptions) => string;
formatCurrency: (amount: number, currency: string) => string;
}
const I18nContext = createContext<I18nContextType>({
locale: 'en-US',
setLocale: () => {},
t: () => '',
formatDate: () => '',
formatCurrency: () => ''
});
// Automatic locale detection
const detectLocale = (): string => {
const stored = localStorage.getItem('preferred-locale');
if (stored) return stored;
const browserLang = navigator.language || 'en-US';
const supported = ['en-US', 'fr-FR', 'es-ES', 'ar-SA'];
return supported.includes(browserLang) ? browserLang : 'en-US';
};
// Locale switching with persistence
const useI18n = () => {
const [locale, setLocale] = useState<string>(detectLocale());
const changeLocale = (newLocale: string) => {
setLocale(newLocale);
localStorage.setItem('preferred-locale', newLocale);
document.documentElement.lang = newLocale;
document.documentElement.dir = newLocale.startsWith('ar') ? 'rtl' : 'ltr';
};
return { locale, setLocale: changeLocale };
};
Translation File Organization
Separate files per language with hierarchical structure:
src/
locales/
en-US.json
fr-FR.json
es-ES.json
ar-SA.json
i18n/
index.ts
utils.ts
types.ts
Translation Loading Strategy
Lazy loading with dynamic imports:
// Dynamic translation loader
const loadTranslations = async (locale: string) => {
try {
const translations = await import(`../locales/${locale}.json`);
return translations.default;
} catch (error) {
console.error(`Failed to load translations for ${locale}`, error);
return {};
}
};
// Translation provider with loading state
const TranslationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [translations, setTranslations] = useState<Record<string, string>>({});
const [loading, setLoading] = useState(true);
useEffect(() => {
const load = async () => {
const loaded = await loadTranslations(detectLocale());
setTranslations(loaded);
setLoading(false);
};
load();
}, []);
const t = (key: string, params?: Record<string, unknown>): string => {
let translation = translations[key] || key;
if (params) {
Object.entries(params).forEach(([param, value]) => {
translation = translation.replace(`{{${param}}}`, String(value));
});
}
return translation;
};
return (
<I18nContext.Provider value={{ locale, t, setLocale, formatDate, formatCurrency }}>
{loading ? <div>Loading translations...</div> : children}
</I18nContext.Provider>
);
};
Accessibility Considerations
Ensuring proper language attributes and screen reader support:
// Language attribute handling for accessibility
const I18nApp = () => {
const { locale } = useI18n();
return (
<div
lang={locale}
dir={locale.startsWith('ar') ? 'rtl' : 'ltr'}
className="i18n-app"
>
<App />
</div>
);
};
// Screen reader friendly date formatting
const FormattedDate: React.FC<{
date: Date;
locale: string;
options?: Intl.DateTimeFormatOptions;
}> = ({ date, locale, options }) => {
const formatted = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
...options
}).format(date);
return (
<span
aria-label={`Date: ${formatted}`}
role="text"
>
{formatted}
</span>
);
};
Testing Strategy
Comprehensive i18n testing with multiple locales:
// Test cases for i18n functionality
describe('I18n', () => {
test('loads translations for supported locales', async () => {
const enTranslations = await loadTranslations('en-US');
expect(enTranslations).toBeDefined();
});
test('handles RTL layout correctly', () => {
const { container } = render(
<div dir="rtl">
<I18nProvider locale="ar-SA">
<TestComponent />
</I18nProvider>
</div>
);
expect(container).toHaveAttribute('dir', 'rtl');
});
test('formats currency correctly in different locales', () => {
const result = formatCurrency(1234.56, 'en-US', 'USD');
expect(result).toBe('$1,234.56');
const resultFr = formatCurrency(1234.56, 'fr-FR', 'EUR');
expect(resultFr).toBe('1 234,56 €');
});
});
12. Monitoring & Observability
Real User Monitoring (RUM) metrics to track
The frontend should capture key RUM metrics to understand user experience and performance:
- First Contentful Paint (FCP): Time from page load to first content rendering
- Largest Contentful Paint (LCP): Time to render the largest image/text element
- First Input Delay (FID): Time from first user interaction to browser response
- Cumulative Layout Shift (CLS): Visual stability of content during load
- Time to Interactive (TTI): When page becomes fully interactive
- Page Load Time: Complete page load duration
These metrics should be collected via the Web Vitals API and sent to monitoring systems with user session context.
Error tracking integration
Integrate with error monitoring tools like Sentry or Datadog to capture:
- Uncaught JavaScript errors: Caught by global error boundaries
- Unhandled promise rejections: Using
window.addEventListener('unhandledrejection') - Network errors: API request failures with status codes
- Console errors: Runtime warnings and errors from console.log
Error reports should include:
- Stack trace with source maps
- User session context (user ID, session ID)
- Browser and device information
- URL path and query parameters
- Request/response payloads where safe
Custom events and user journey tracking
Implement custom event tracking for key user actions:
// Example custom event tracking system
export interface UserEvent {
name: string;
timestamp: number;
userId?: string;
sessionId: string;
properties?: Record<string, any>;
}
export const trackUserEvent = (event: UserEvent) => {
// Send to analytics backend or real-time monitoring service
const payload = {
...event,
timestamp: Date.now(),
userAgent: navigator.userAgent,
viewport: `${window.innerWidth}x${window.innerHeight}`
};
// Example: send to custom analytics endpoint
fetch('/api/analytics/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
};
Performance monitoring and regression detection
Implement performance baselines and alerting:
- Baseline tracking: Store historical performance metrics per route
- Regression detection: Alert when performance degrades beyond threshold (e.g., 10% increase in LCP)
- Route-specific metrics: Track performance per page or feature
- Threshold-based alerts: Configure thresholds for key metrics (LCP > 2.5s, FID > 100ms)
Implementation approach
// Performance monitoring hook - capture key metrics and send to observability backend
import { useEffect, useRef } from 'react';
export const usePerformanceMonitoring = () => {
const observerRef = useRef<PerformanceObserver | null>(null);
useEffect(() => {
// Setup Web Vitals tracking
if ('PerformanceObserver' in window && 'getEntriesByType' in performance) {
observerRef.current = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.entryType === 'largest-contentful-paint') {
// Send LCP metric to monitoring service
sendMetric('lcp', entry.startTime, {
value: entry.value,
element: entry.element?.tagName || 'unknown'
});
}
});
});
observerRef.current.observe({ entryTypes: ['largest-contentful-paint'] });
}
return () => {
if (observerRef.current) {
observerRef.current.disconnect();
}
};
}, []);
const sendMetric = (metricName: string, value: number, properties?: any) => {
// Send to monitoring backend or analytics service
console.log(`[RUM] ${metricName}:`, value, properties);
};
};
Integration with backend monitoring
Backend services should correlate frontend metrics with server-side performance:
- Request correlation: Include request IDs in both frontend and backend logs
- Session tracking: Match user sessions across frontend and backend services
- Error context: Include frontend error context in backend error reports
Alerting strategy
Configure alerting for:
- High error rates: >5% of requests failing
- Performance regressions: >10% degradation in key metrics
- Critical user flows: Failures in authentication, checkout, or content loading
Alerts should be sent to:
- Slack/Teams channels for immediate visibility
- Email notifications for critical issues
- Incident management systems for tracking and resolution
Data privacy considerations
All monitoring data should:
- Exclude sensitive user information (passwords, PII)
- Comply with privacy regulations (GDPR, CCPA)
- Allow user opt-out mechanisms where required
- Be aggregated and anonymized where possible
Dashboard and visualization
Create dashboards to visualize:
- Real-time performance metrics
- Error rate trends over time
- User journey completion rates
- Geographic and device-based performance differences
These dashboards should be accessible to engineering teams for proactive issue detection and resolution.
13. File & Folder Structure (brief — implementation detail, not primary LLD interview content)
Proposed Directory Structure
/src
/features
/auth
/components
/hooks
/services
/types
/dashboard
/components
/hooks
/services
/types
/shared
/components
/hooks
/utils
/types
/constants
/core
/api
/auth
/error
/storage
/routes
/styles
/types
Module Boundaries and Import Rules
- Feature modules are self-contained with internal imports only
- Shared components/utilities can be imported by any feature module
- Core modules (auth, api) are top-level and can be imported by features but not vice versa
- No cross-feature imports to maintain module isolation
Shared vs. Feature-Specific Code Separation
- Shared: Reusable UI components, utility functions, type definitions, constants
- Feature-specific: Components, services, hooks, and types that are only relevant to a specific feature area
- Core: Infrastructure-level code (auth guards, API clients) that all features depend on
This structure enables clear separation of concerns while maintaining flexibility for feature development and code reuse. The feature-based organization allows teams to own their modules independently while maintaining consistent patterns across the codebase.
14. Dependencies & Third-Party Libraries
Key Libraries Selected with Justification
1. Axios - Primary HTTP client for API communication
- Justification: Battle-tested, robust error handling, request/response interceptors, and extensive ecosystem support. Provides built-in CSRF token handling through interceptors.
- Bundle size: ~12KB gzipped
- Security: Built-in CORS support, request/response transformation capabilities
2. React Hook Form - Form state management and validation
- Justification: Optimized for performance, excellent TypeScript support, built-in validation with Zod integration, and minimal re-renders through field-level control.
- Bundle size: ~15KB gzipped
- Security: Prevents XSS through proper input sanitization and validation
3. React Query (TanStack Query) - Server state management
- Justification: Excellent caching strategy, automatic refetching, optimistic updates, and built-in error recovery. Provides seamless integration with React's concurrent rendering.
- Bundle size: ~18KB gzipped
- Security: Handles sensitive data in cache with proper cleanup and memory management
4. React Router v6 - Client-side routing
- Justification: Clean API, nested routing support, built-in lazy loading, and excellent integration with React Query for data fetching.
- Bundle size: ~10KB gzipped
- Security: Built-in route protection and authentication guards
5. Styled Components / Tailwind CSS - Styling solution
- Justification: Component-scoped styling prevents CSS pollution, supports theming, and enables better developer experience with type safety.
- Bundle size: ~20KB (Tailwind) / ~15KB (Styled Components)
- Security: No runtime security concerns, CSS isolation prevents XSS via CSS injection
Alternatives Considered and Rejected
1. Fetch API + Custom Wrapper
- Rejected because: Requires significant boilerplate for error handling, caching, and interceptors. Would need to build CSRF protection from scratch.
2. Redux Toolkit + Redux-Saga
- Rejected because: Overkill for this feature's scope. React Query provides better server state management with less boilerplate and better performance characteristics.
3. Formik
- Rejected because: Less performant than React Hook Form, particularly with large forms. More verbose API and less TypeScript support.
4. Emotion
- Rejected because: While powerful, it introduces more complexity than Tailwind for this use case and requires additional configuration for security.
5. React-Table / react-virtual
- Rejected because: Not relevant to CSRF protection feature, but would be considered for list rendering if needed.
Licensing and Bundle Size Impact Assessment
Licensing Compliance:
- All selected libraries are MIT or BSD-3-Clause licensed, suitable for enterprise use
- No GPL dependencies that would require open-sourcing our codebase
- Regular security scanning integration in CI pipeline to detect license violations
Bundle Size Budget:
- Target total vendor bundle size: ~80KB (gzipped) for core features
- Current selection accounts for ~55KB of that total, leaving room for future feature additions
- Tree-shaking and code splitting strategies will reduce actual runtime bundle size
Performance Impact:
- All libraries are optimized for production builds with proper minification
- React Query's automatic garbage collection prevents memory leaks
- Axios interceptors are designed for minimal performance overhead
Security Considerations:
- All libraries undergo security audits before inclusion
- Regular dependency updates with security patching strategy (monthly security scans)
- No known vulnerabilities in current library versions within our usage scope
15. Open Questions, Risks & Trade-offs
Decisions that need product/engineering alignment
- CSRF token storage location: Should tokens be stored in HTTP-only cookies, localStorage, or sessionStorage? This decision directly impacts the security model and cross-origin support requirements.
- Token refresh strategy: How often should CSRF tokens be rotated? Should they be tied to session lifecycle or have shorter TTLs?
- Cross-origin support: Will the application need to support cross-origin requests? If so, how should CSRF protection be handled for these scenarios?
- Authentication method alignment: How does CSRF protection integrate with the existing authentication system (OAuth, JWT, session-based)?
Known trade-offs made in this design
- Security vs. User Experience: Using longer-lived tokens improves UX but increases security surface. Shorter tokens provide better security but may cause more authentication interruptions.
- Performance vs. Security: Token validation adds processing overhead on every request, but removing it would make the application more vulnerable to CSRF attacks.
- Complexity vs. Coverage: Implementing comprehensive CSRF protection across all request types adds complexity but ensures complete coverage of attack vectors.
- State Management vs. Token Storage: Storing tokens in React context provides easy access but may conflict with server-side rendering or client-side token management strategies.
Risks and mitigations
- Token Expiration Risk: If tokens expire too quickly, users may face authentication interruptions during long sessions. Mitigation: Implement token refresh mechanisms with grace periods and proper error handling.
- Cross-Origin Request Risk: If not properly configured, CSRF protection may break legitimate cross-origin requests. Mitigation: Implement proper CORS headers and token validation for cross-origin scenarios.
- Token Invalidation Risk: If tokens are not properly invalidated on session changes, they could be reused maliciously. Mitigation: Implement proper token invalidation on logout and session changes.
- State Synchronization Risk: Client-side token state may get out of sync with server state. Mitigation: Implement robust error handling and automatic recovery mechanisms.
Technical debt acknowledged
- Token Management Overhead: The current implementation requires additional state management and validation logic that could be simplified with better backend integration or token generation strategies.
- Testing Coverage Gap: While the core CSRF protection logic is covered, edge cases around token expiration and cross-origin scenarios may require additional test coverage that isn't fully addressed in this design.
- Framework Dependency: The current implementation assumes React context and hooks, which could create challenges if the application later migrates to a different frontend framework or state management system.
- Session Management Coupling: The CSRF protection implementation is tightly coupled to session management, which could create maintenance challenges if these systems evolve independently.