# React Frontend Architecture Guide
## Building Enterprise Applications with Three-Layer Service Architecture
**Version:** 2.0.0
**React:** 19.1.0
**Build Tool:** Vite 7.1.2
**Styling:** Tailwind CSS 4.1.12
---
## Table of Contents
1. [Project Overview](#project-overview)
2. [Technology Stack](#technology-stack)
3. [Project Structure](#project-structure)
4. [Three-Layer Service Architecture](#three-layer-service-architecture)
5. [Dependency Injection System](#dependency-injection-system)
6. [Constants Management](#constants-management)
7. [Component Architecture](#component-architecture)
8. [Authentication & Authorization](#authentication--authorization)
9. [Routing & Navigation](#routing--navigation)
10. [State Management & Caching](#state-management--caching)
11. [Styling & Theming](#styling--theming)
12. [Build Configuration](#build-configuration)
13. [Complete Setup Guide](#complete-setup-guide)
14. [Best Practices & Patterns](#best-practices--patterns)
---
## Project Overview
This architecture guide documents a production-grade React application built for enterprise use. The application follows a strict **three-layer service architecture** (API, Storage, Manager) with custom dependency injection, comprehensive constants management, and zero tolerance for magic values.
### Core Principles
1. **No Magic Values**: Every number, string, or configuration value must be defined as a constant
2. **Service Layer Separation**: Clear boundaries between API, Storage, and Manager layers
3. **Dependency Injection**: Custom DI system without third-party libraries
4. **Component Reusability**: Extensive component library organized by purpose
5. **Type Safety**: PropTypes for runtime type checking
6. **Performance**: Code splitting, caching, and optimization strategies
---
## Technology Stack
### Core Dependencies
```json
{
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.8.0",
"vite": "^7.1.2",
"tailwindcss": "^4.1.12"
}
```
### Key Libraries
- **HTTP Client**: `axios` (^1.11.0) - API communication with interceptors
- **Date Handling**: `luxon` (^3.7.1) - Date manipulation and formatting
- **Case Conversion**: `humps` (^2.0.1) - camelCase ↔ snake_case conversion
- **Icons**: `@heroicons/react` (^2.2.0) - SVG icon library
- **QR Codes**: `qrcode.react` (^4.2.0) - 2FA QR code generation
- **Utility**: `clsx` (^2.1.1) - Conditional className utility
### Development Tools
- **Linting**: ESLint 9 with React plugins
- **Build Optimization**: Terser for minification
- **CSS Processing**: PostCSS with Autoprefixer
---
## Project Structure
```
web/frontend/
├── index.html # Entry HTML file
├── package.json # Dependencies and scripts
├── vite.config.js # Vite build configuration
├── tailwind.config.js # Tailwind CSS configuration
├── eslint.config.js # ESLint configuration
│
├── public/ # Static assets
│ ├── favicon.ico
│ └── assets/
│
└── src/
├── main.jsx # Application entry point
├── AppRouter.jsx # Main routing configuration
│
├── components/ # Reusable components
│ ├── UI/ # Pure UI components
│ │ ├── Button/
│ │ ├── Input/
│ │ ├── Modal/
│ │ ├── Table/
│ │ └── ...
│ ├── UIX/ # Enhanced UI components
│ │ ├── EntityListPage/
│ │ ├── EntityDetailPage/
│ │ └── ...
│ ├── business/ # Domain-aware components
│ │ ├── CustomerSelect/
│ │ ├── OrderDisplay/
│ │ └── ...
│ └── Layout/ # Layout components
│ ├── Sidebar.jsx
│ ├── Header.jsx
│ └── Footer.jsx
│
├── pages/ # Page components (route handlers)
│ ├── Admin/
│ │ ├── Customer/
│ │ │ ├── List/
│ │ │ │ └── Page.jsx
│ │ │ ├── Detail/
│ │ │ │ └── Page.jsx
│ │ │ ├── Create/
│ │ │ │ └── Page.jsx
│ │ │ └── Update/
│ │ │ └── Page.jsx
│ │ └── ...
│ ├── Dashboard/
│ └── Auth/
│
├── services/ # Service layer (CRITICAL)
│ ├── Services.jsx # DI container
│ ├── Config/
│ │ └── APIConfig.js
│ ├── API/ # HTTP communication layer
│ │ ├── CustomerAPI.js
│ │ ├── AuthAPI.js
│ │ └── ...
│ ├── Storage/ # Local persistence layer
│ │ ├── CustomerStorage.js
│ │ ├── TokenStorage.js
│ │ └── ...
│ ├── Manager/ # Business logic layer
│ │ ├── CustomerManager.js
│ │ ├── AuthManager.js
│ │ └── ...
│ └── Helpers/
│ └── AuthenticatedAxios.js
│
├── constants/ # Constants (NO MAGIC VALUES!)
│ ├── Customer.js
│ ├── Authentication.js
│ ├── UI.js
│ ├── Roles.js
│ └── ...
│
├── utils/ # Utility functions
│ └── serviceUtils.js
│
├── styles/ # Global styles
│ └── app.css
│
└── assets/ # Static assets (images, fonts)
```
---
## Three-Layer Service Architecture
The most critical aspect of this architecture is the **three-layer service pattern**. This pattern ensures:
- Clear separation of concerns
- Testability
- Maintainability
- Consistent error handling
- Caching strategies
### Layer 1: API Layer (`src/services/API/`)
**Purpose**: HTTP communication with backend services
**Responsibilities**:
- Making HTTP requests using Axios
- Request/response transformation (camelCase ↔ snake_case)
- Error handling and formatting
- Endpoint configuration
**Example: CustomerAPI.js**
```javascript
// src/services/API/CustomerAPI.js
import { camelizeKeys, decamelizeKeys, decamelize } from "humps";
import { createAuthenticatedAxios } from "../Helpers/AuthenticatedAxios";
import { DateTime } from "luxon";
export class CustomerAPI {
constructor(baseURL, endpoints, tokenStorage) {
this.baseURL = baseURL;
this.endpoints = endpoints;
this.tokenStorage = tokenStorage;
}
/**
* Gets list of customers with filtering and pagination
*/
async getCustomers(params = {}, onUnauthorizedCallback = null) {
try {
// Create authenticated axios instance
const authenticatedAxios = createAuthenticatedAxios(
this.baseURL,
this.tokenStorage,
onUnauthorizedCallback
);
// Build query parameters
const queryParams = new URLSearchParams();
if (params.page) queryParams.append("page", params.page);
if (params.limit) queryParams.append("page_size", params.limit);
if (params.search) queryParams.append("search", params.search);
// Make API call
const response = await authenticatedAxios.get(
`${this.endpoints.CUSTOMERS}?${queryParams.toString()}`
);
// Convert snake_case to camelCase
const data = camelizeKeys(response.data);
// Format dates
if (data.results) {
data.results.forEach((item) => {
if (item.createdAt) {
item.createdAt = DateTime.fromISO(item.createdAt)
.toLocaleString(DateTime.DATETIME_MED);
}
});
}
return data;
} catch (error) {
throw this._formatError(error);
}
}
/**
* Creates a new customer
*/
async createCustomer(customerData, onUnauthorizedCallback = null) {
try {
const authenticatedAxios = createAuthenticatedAxios(
this.baseURL,
this.tokenStorage,
onUnauthorizedCallback
);
// Convert camelCase to snake_case for backend
let decamelizedData = decamelizeKeys(customerData);
const response = await authenticatedAxios.post(
this.endpoints.CUSTOMERS,
decamelizedData
);
return camelizeKeys(response.data);
} catch (error) {
throw this._formatError(error);
}
}
/**
* Gets customer detail by ID
*/
async getCustomerDetail(customerId, onUnauthorizedCallback = null) {
try {
if (!customerId) {
throw { customerId: "Valid customer ID is required" };
}
const authenticatedAxios = createAuthenticatedAxios(
this.baseURL,
this.tokenStorage,
onUnauthorizedCallback
);
const url = this.endpoints.CUSTOMER_DETAIL.replace("{id}", customerId);
const response = await authenticatedAxios.get(url);
return camelizeKeys(response.data);
} catch (error) {
throw this._formatError(error);
}
}
/**
* Updates a customer
*/
async updateCustomer(customerId, customerData, onUnauthorizedCallback = null) {
try {
const authenticatedAxios = createAuthenticatedAxios(
this.baseURL,
this.tokenStorage,
onUnauthorizedCallback
);
let decamelizedData = decamelizeKeys(customerData);
decamelizedData.id = customerId;
const url = this.endpoints.CUSTOMER_DETAIL.replace("{id}", customerId);
const response = await authenticatedAxios.put(url, decamelizedData);
return camelizeKeys(response.data);
} catch (error) {
throw this._formatError(error);
}
}
/**
* Deletes a customer
*/
async deleteCustomer(customerId, onUnauthorizedCallback = null) {
try {
const authenticatedAxios = createAuthenticatedAxios(
this.baseURL,
this.tokenStorage,
onUnauthorizedCallback
);
const url = this.endpoints.CUSTOMER_DETAIL.replace("{id}", customerId);
const response = await authenticatedAxios.delete(url);
return camelizeKeys(response.data);
} catch (error) {
throw this._formatError(error);
}
}
/**
* Formats errors consistently
*/
_formatError(error) {
let errorData = error.response?.data || error.response || error;
return camelizeKeys(errorData);
}
}
```
### Layer 2: Storage Layer (`src/services/Storage/`)
**Purpose**: Local data persistence, caching, and state management
**Responsibilities**:
- localStorage operations
- sessionStorage operations
- In-memory caching
- Cache invalidation
- Data persistence strategies
**Example: CustomerStorage.js**
```javascript
// src/services/Storage/CustomerStorage.js
export class CustomerStorage {
constructor() {
this.CUSTOMERS_CACHE_KEY = "MAPLEPRESS_CUSTOMERS_CACHE";
this.CUSTOMERS_TIMESTAMP_KEY = "MAPLEPRESS_CUSTOMERS_TIMESTAMP";
this.DEFAULT_CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
// In-memory cache for current session
this.memoryCache = {
customers: null,
customersTimestamp: null,
isCustomersLoading: false,
};
}
/**
* Gets customers from cache (memory first, then localStorage)
*/
getCustomersFromCache(maxAge = this.DEFAULT_CACHE_DURATION) {
// Check memory cache first (fastest)
if (this._isMemoryCacheValid(maxAge)) {
console.log("Using memory cache for customers");
return this.memoryCache.customers;
}
// Check localStorage cache
try {
const cachedCustomers = localStorage.getItem(this.CUSTOMERS_CACHE_KEY);
const cachedTimestamp = localStorage.getItem(this.CUSTOMERS_TIMESTAMP_KEY);
if (cachedCustomers && cachedTimestamp) {
const timestamp = parseInt(cachedTimestamp);
const age = Date.now() - timestamp;
if (age < maxAge) {
const customersData = JSON.parse(cachedCustomers);
// Update memory cache
this.memoryCache.customers = customersData;
this.memoryCache.customersTimestamp = timestamp;
console.log("Using localStorage cache for customers");
return customersData;
}
}
} catch (error) {
console.error("Error reading from localStorage", error);
this._clearLocalStorageCache();
}
return null;
}
/**
* Saves customers to cache (both memory and localStorage)
*/
saveCustomersToCache(customersData) {
if (!customersData) {
console.warn("Attempted to save null/undefined data");
return;
}
const timestamp = Date.now();
// Save to memory cache
this.memoryCache.customers = customersData;
this.memoryCache.customersTimestamp = timestamp;
this.memoryCache.isCustomersLoading = false;
// Save to localStorage
try {
localStorage.setItem(
this.CUSTOMERS_CACHE_KEY,
JSON.stringify(customersData)
);
localStorage.setItem(
this.CUSTOMERS_TIMESTAMP_KEY,
timestamp.toString()
);
console.log("Customers cached successfully", {
timestamp: new Date(timestamp).toISOString(),
count: customersData.results ? customersData.results.length : 0,
});
} catch (error) {
console.error("Error saving to localStorage", error);
}
}
/**
* Clears customers cache
*/
clearCustomersCache() {
this.memoryCache.customers = null;
this.memoryCache.customersTimestamp = null;
this.memoryCache.isCustomersLoading = false;
this._clearLocalStorageCache();
console.log("Customers cache cleared");
}
/**
* Sets loading state
*/
setCustomersCacheLoading(isLoading) {
this.memoryCache.isCustomersLoading = isLoading;
}
/**
* Gets loading state
*/
isCustomersCacheLoading() {
return this.memoryCache.isCustomersLoading;
}
/**
* Private helper methods
*/
_isMemoryCacheValid(maxAge) {
if (!this.memoryCache.customers || !this.memoryCache.customersTimestamp) {
return false;
}
const age = Date.now() - this.memoryCache.customersTimestamp;
return age < maxAge;
}
_clearLocalStorageCache() {
localStorage.removeItem(this.CUSTOMERS_CACHE_KEY);
localStorage.removeItem(this.CUSTOMERS_TIMESTAMP_KEY);
}
}
```
### Layer 3: Manager Layer (`src/services/Manager/`)
**Purpose**: Business logic and orchestration
**Responsibilities**:
- Combining API and Storage services
- Business rule implementation
- Validation logic
- Cache coordination
- Providing clean interface to components
**Example: CustomerManager.js**
```javascript
// src/services/Manager/CustomerManager.js
export class CustomerManager {
constructor(customerAPI, customerStorage) {
this.customerAPI = customerAPI;
this.customerStorage = customerStorage;
}
/**
* Gets list of customers with caching
*/
async getCustomers(
params = {},
onUnauthorizedCallback = null,
forceRefresh = false
) {
try {
// Check cache first unless force refresh
if (!forceRefresh) {
const cachedCustomers = this.customerStorage.getCustomersFromCache();
if (cachedCustomers) {
return cachedCustomers;
}
}
// Prevent multiple simultaneous requests
if (this.customerStorage.isCustomersCacheLoading()) {
console.log("Customers request already in progress");
return this._waitForCurrentRequest();
}
this.customerStorage.setCustomersCacheLoading(true);
try {
// Validate parameters
const validatedParams = this._validateCustomersParams(params);
// Fetch from API
const customersData = await this.customerAPI.getCustomers(
validatedParams,
onUnauthorizedCallback
);
// Save to cache
this.customerStorage.saveCustomersToCache(customersData);
return customersData;
} finally {
this.customerStorage.setCustomersCacheLoading(false);
}
} catch (error) {
this.customerStorage.setCustomersCacheLoading(false);
console.error("Failed to get customers", error);
throw error;
}
}
/**
* Creates a new customer with validation
*/
async createCustomer(customerData, onUnauthorizedCallback = null) {
try {
// Validate data
const validationErrors = this._validateCustomerData(customerData, true);
if (Object.keys(validationErrors).length > 0) {
throw validationErrors;
}
// Call API
const createdCustomer = await this.customerAPI.createCustomer(
customerData,
onUnauthorizedCallback
);
// Clear cache since data changed
this.customerStorage.clearCustomersCache();
return createdCustomer;
} catch (error) {
console.error("Failed to create customer", error);
throw error;
}
}
/**
* Gets customer detail
*/
async getCustomerDetail(customerId, onUnauthorizedCallback = null) {
try {
const validationError = this._validateCustomerId(customerId);
if (validationError) {
throw validationError;
}
const customerData = await this.customerAPI.getCustomerDetail(
customerId,
onUnauthorizedCallback
);
return customerData;
} catch (error) {
console.error("Failed to get customer detail", error);
throw error;
}
}
/**
* Updates a customer
*/
async updateCustomer(
customerId,
customerData,
onUnauthorizedCallback = null
) {
try {
// Validate ID
const idError = this._validateCustomerId(customerId);
if (idError) throw idError;
// Validate data
const validationErrors = this._validateCustomerData(customerData);
if (Object.keys(validationErrors).length > 0) {
throw validationErrors;
}
// Call API
const updatedCustomer = await this.customerAPI.updateCustomer(
customerId,
customerData,
onUnauthorizedCallback
);
// Clear cache
this.customerStorage.clearCustomersCache();
return updatedCustomer;
} catch (error) {
console.error("Failed to update customer", error);
throw error;
}
}
/**
* Deletes a customer
*/
async deleteCustomer(customerId, onUnauthorizedCallback = null) {
try {
const validationError = this._validateCustomerId(customerId);
if (validationError) throw validationError;
const deleteResponse = await this.customerAPI.deleteCustomer(
customerId,
onUnauthorizedCallback
);
// Clear cache
this.customerStorage.clearCustomersCache();
return deleteResponse;
} catch (error) {
console.error("Failed to delete customer", error);
throw error;
}
}
/**
* Clears cache
*/
clearCustomersCache() {
this.customerStorage.clearCustomersCache();
}
/**
* Private validation methods
*/
_validateCustomerId(customerId) {
if (!customerId ||
(typeof customerId !== "string" && typeof customerId !== "number")) {
return { customerId: "Valid customer ID is required" };
}
return null;
}
_validateCustomersParams(params) {
const validatedParams = {};
// Validate pagination
if (params.page && typeof params.page === "number" && params.page > 0) {
validatedParams.page = params.page;
}
if (params.limit && typeof params.limit === "number" &&
params.limit > 0 && params.limit <= 1000) {
validatedParams.limit = params.limit;
}
// Validate search
if (params.search && typeof params.search === "string" &&
params.search.trim()) {
validatedParams.search = params.search.trim();
}
return validatedParams;
}
_validateCustomerData(customerData, isCreate = false) {
const errors = {};
if (!customerData || typeof customerData !== "object") {
errors.general = "Customer data is required";
return errors;
}
// Validate first name
if (!customerData.firstName || !customerData.firstName.trim()) {
errors.firstName = "First name is required";
}
// Validate last name
if (!customerData.lastName || !customerData.lastName.trim()) {
errors.lastName = "Last name is required";
}
// Validate email
if (!customerData.email || !customerData.email.trim()) {
errors.email = "Email is required";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(customerData.email)) {
errors.email = "Invalid email format";
}
return errors;
}
_waitForCurrentRequest() {
return new Promise((resolve, reject) => {
const checkInterval = setInterval(() => {
if (!this.customerStorage.isCustomersCacheLoading()) {
clearInterval(checkInterval);
const cachedData = this.customerStorage.getCustomersFromCache();
if (cachedData) {
resolve(cachedData);
} else {
reject(new Error("Request failed"));
}
}
}, 100);
setTimeout(() => {
clearInterval(checkInterval);
reject(new Error("Request timeout"));
}, 30000);
});
}
}
```
---
## Dependency Injection System
The application uses a custom dependency injection container without third-party libraries.
### Service Registration (`src/services/Services.jsx`)
```javascript
// src/services/Services.jsx
import React, { createContext, useContext, useMemo } from "react";
import { getAPIBaseURL, API_ENDPOINTS } from "./Config/APIConfig";
// Import all Storage services
import { TokenStorage } from "./Storage/TokenStorage";
import { CustomerStorage } from "./Storage/CustomerStorage";
// Import all API services
import { AuthAPI } from "./API/AuthAPI";
import { CustomerAPI } from "./API/CustomerAPI";
// Import all Manager services
import { AuthManager } from "./Manager/AuthManager";
import { CustomerManager } from "./Manager/CustomerManager";
/**
* Service container class that manages all service instances
*/
class Services {
constructor() {
const baseURL = getAPIBaseURL();
// Storage Services
this.tokenStorage = new TokenStorage();
this.customerStorage = new CustomerStorage();
// API Services (depend on tokenStorage)
this.authAPI = new AuthAPI(baseURL, API_ENDPOINTS, this.tokenStorage);
this.customerAPI = new CustomerAPI(
baseURL,
API_ENDPOINTS,
this.tokenStorage
);
// Manager Services (depend on API and Storage)
this.authManager = new AuthManager(this.authAPI, this.tokenStorage);
this.customerManager = new CustomerManager(
this.customerAPI,
this.customerStorage
);
}
}
// Create React Context for DI
const ServiceContext = createContext(null);
/**
* ServiceProvider component wraps the application
*/
export function ServiceProvider({ children }) {
const services = useMemo(() => new Services(), []);
return (
Loading...
) : (