monorepo/web/maplepress-frontend/docs/FRONTEND_ARCHITECTURE.md

52 KiB

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
  2. Technology Stack
  3. Project Structure
  4. Three-Layer Service Architecture
  5. Dependency Injection System
  6. Constants Management
  7. Component Architecture
  8. Authentication & Authorization
  9. Routing & Navigation
  10. State Management & Caching
  11. Styling & Theming
  12. Build Configuration
  13. Complete Setup Guide
  14. 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

{
  "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

// 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

// 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

// 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)

// 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 (
    <ServiceContext.Provider value={services}>
      {children}
    </ServiceContext.Provider>
  );
}

/**
 * Hook to access services in components
 */
export function useServices() {
  const context = useContext(ServiceContext);
  if (!context) {
    throw new Error("useServices must be used within ServiceProvider");
  }
  return context;
}

Using Services in Components

// Example: CustomerListPage.jsx
import React, { useState, useEffect } from "react";
import { useServices } from "../services/Services";

function CustomerListPage() {
  const { customerManager } = useServices();
  const [customers, setCustomers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchCustomers();
  }, []);

  async function fetchCustomers() {
    try {
      setLoading(true);
      const data = await customerManager.getCustomers(
        { page: 1, limit: 20 },
        handleUnauthorized
      );
      setCustomers(data.results);
    } catch (error) {
      console.error("Failed to fetch customers:", error);
    } finally {
      setLoading(false);
    }
  }

  function handleUnauthorized() {
    // Handle unauthorized access (redirect to login)
    window.location.href = "/login";
  }

  return (
    <div>
      <h1>Customers</h1>
      {loading ? (
        <p>Loading...</p>
      ) : (
        <ul>
          {customers.map((customer) => (
            <li key={customer.id}>
              {customer.firstName} {customer.lastName}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

Constants Management

CRITICAL RULE: The application has ZERO TOLERANCE for magic numbers or strings. Every value must be defined as a constant.

Constants Structure

src/constants/
├── Customer.js              # Customer-related constants
├── Authentication.js        # Auth constants
├── Roles.js                # User role constants
├── UI.js                   # UI-related constants
├── Order.js                # Order constants
├── Event.js                # Event constants
├── Financial.js            # Financial constants
├── Storage.js              # Storage key constants
└── ...

Example: Customer Constants (src/constants/Customer.js)

// src/constants/Customer.js

// Customer Status
export const CUSTOMER_STATUS = {
  ACTIVE: 1,
  ARCHIVED: 2,
};

// Customer Status Labels
export const CUSTOMER_STATUS_LABELS = {
  [CUSTOMER_STATUS.ACTIVE]: "Active",
  [CUSTOMER_STATUS.ARCHIVED]: "Archived",
};

// Customer Types
export const CUSTOMER_TYPE_VALUES = {
  EDUCATIONAL: "educational",
  CORPORATE: "corporate",
  NON_PROFIT: "non_profit",
  GOVERNMENT: "government",
};

// Customer Type Labels
export const CUSTOMER_TYPES = {
  EDUCATIONAL: "Educational",
  CORPORATE: "Corporate",
  NON_PROFIT: "Non-Profit",
  GOVERNMENT: "Government",
};

// Customer Type Options for Dropdowns
export const CUSTOMER_TYPE_OPTIONS = [
  { value: CUSTOMER_TYPE_VALUES.EDUCATIONAL, label: CUSTOMER_TYPES.EDUCATIONAL },
  { value: CUSTOMER_TYPE_VALUES.CORPORATE, label: CUSTOMER_TYPES.CORPORATE },
  { value: CUSTOMER_TYPE_VALUES.NON_PROFIT, label: CUSTOMER_TYPES.NON_PROFIT },
  { value: CUSTOMER_TYPE_VALUES.GOVERNMENT, label: CUSTOMER_TYPES.GOVERNMENT },
];

// Phone Types
export const CUSTOMER_PHONE_TYPES = {
  MOBILE: 1,
  WORK: 2,
  HOME: 3,
};

export const CUSTOMER_PHONE_TYPE_LABELS = {
  [CUSTOMER_PHONE_TYPES.MOBILE]: "Mobile",
  [CUSTOMER_PHONE_TYPES.WORK]: "Work",
  [CUSTOMER_PHONE_TYPES.HOME]: "Home",
};

// Pagination
export const CUSTOMERS_PER_PAGE = 20;
export const DEFAULT_PAGE = 1;

// Form Validation
export const CUSTOMER_VALIDATION = {
  NAME_MIN_LENGTH: 2,
  NAME_MAX_LENGTH: 100,
  EMAIL_MAX_LENGTH: 255,
  PHONE_MAX_LENGTH: 20,
  ADDRESS_MAX_LENGTH: 500,
  COMMENT_MAX_LENGTH: 638,
};

// Cache Configuration
export const CUSTOMER_CACHE_EXPIRY = {
  LIST: 5 * 60 * 1000,    // 5 minutes
  DETAIL: 10 * 60 * 1000,  // 10 minutes
  SEARCH: 3 * 60 * 1000,   // 3 minutes
};

// Countries
export const CUSTOMER_COUNTRIES = {
  CA: "CA",
  US: "US",
  MX: "MX",
};

export const CUSTOMER_COUNTRY_LABELS = {
  [CUSTOMER_COUNTRIES.CA]: "Canada",
  [CUSTOMER_COUNTRIES.US]: "United States",
  [CUSTOMER_COUNTRIES.MX]: "Mexico",
};

Example: Authentication Constants (src/constants/Authentication.js)

// src/constants/Authentication.js

// HTTP Status Codes
export const HTTP_STATUS = {
  OK: 200,
  CREATED: 201,
  NO_CONTENT: 204,
  BAD_REQUEST: 400,
  UNAUTHORIZED: 401,
  FORBIDDEN: 403,
  NOT_FOUND: 404,
  INTERNAL_SERVER_ERROR: 500,
};

// Token Types
export const AUTH_TOKEN_TYPE = {
  JWT: "JWT",
  BEARER: "Bearer",
};

// HTTP Headers
export const HTTP_HEADERS = {
  CONTENT_TYPE_JSON: "application/json",
  ACCEPT_JSON: "application/json",
  CONTENT_TYPE_MULTIPART: "multipart/form-data",
};

// Token Storage Keys
export const TOKEN_STORAGE_KEYS = {
  ACCESS_TOKEN: "MAPLEPRESS_ACCESS_TOKEN",
  REFRESH_TOKEN: "MAPLEPRESS_REFRESH_TOKEN",
  USER_DATA: "MAPLEPRESS_USER_DATA",
};

// Session Timeout
export const SESSION_TIMEOUT = {
  WARNING: 5 * 60 * 1000,  // 5 minutes warning
  LOGOUT: 15 * 60 * 1000,  // 15 minutes auto logout
};

Using Constants in Components

import { CUSTOMER_STATUS, CUSTOMER_STATUS_LABELS } from "@constants/Customer";

function CustomerBadge({ status }) {
  const label = CUSTOMER_STATUS_LABELS[status];
  const isActive = status === CUSTOMER_STATUS.ACTIVE;

  return (
    <span className={isActive ? "badge-green" : "badge-gray"}>
      {label}
    </span>
  );
}

Component Architecture

Components are organized into three main categories:

1. UI Components (src/components/UI/)

Pure, stateless, reusable components with no business logic.

UI/
├── Button/
│   └── Button.jsx
├── Input/
│   └── Input.jsx
├── Modal/
│   └── Modal.jsx
├── Table/
│   └── Table.jsx
├── Card/
│   └── Card.jsx
├── Alert/
│   └── Alert.jsx
└── ...

Example: Button Component

// src/components/UI/Button/Button.jsx
import React from "react";
import PropTypes from "prop-types";
import clsx from "clsx";

export function Button({
  children,
  variant = "primary",
  size = "md",
  disabled = false,
  loading = false,
  onClick,
  type = "button",
  className,
  ...rest
}) {
  const baseStyles = "inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2";

  const variantStyles = {
    primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
    secondary: "bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500",
    danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
    outline: "border-2 border-gray-300 text-gray-700 hover:bg-gray-50",
  };

  const sizeStyles = {
    sm: "px-3 py-1.5 text-sm",
    md: "px-4 py-2 text-base",
    lg: "px-6 py-3 text-lg",
  };

  const disabledStyles = "opacity-50 cursor-not-allowed";

  return (
    <button
      type={type}
      disabled={disabled || loading}
      onClick={onClick}
      className={clsx(
        baseStyles,
        variantStyles[variant],
        sizeStyles[size],
        (disabled || loading) && disabledStyles,
        className
      )}
      {...rest}
    >
      {loading && <Spinner className="mr-2" />}
      {children}
    </button>
  );
}

Button.propTypes = {
  children: PropTypes.node.isRequired,
  variant: PropTypes.oneOf(["primary", "secondary", "danger", "outline"]),
  size: PropTypes.oneOf(["sm", "md", "lg"]),
  disabled: PropTypes.bool,
  loading: PropTypes.bool,
  onClick: PropTypes.func,
  type: PropTypes.oneOf(["button", "submit", "reset"]),
  className: PropTypes.string,
};

2. Business Components (src/components/business/)

Domain-aware components with service integration.

// src/components/business/CustomerSelect.jsx
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { useServices } from "@services/Services";

export function CustomerSelect({ value, onChange, placeholder = "Select customer" }) {
  const { customerManager } = useServices();
  const [customers, setCustomers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchCustomers();
  }, []);

  async function fetchCustomers() {
    try {
      setLoading(true);
      const data = await customerManager.getCustomers({ limit: 100 });
      setCustomers(data.results || []);
    } catch (error) {
      console.error("Failed to fetch customers:", error);
    } finally {
      setLoading(false);
    }
  }

  return (
    <select
      value={value}
      onChange={(e) => onChange(e.target.value)}
      className="form-select"
      disabled={loading}
    >
      <option value="">{loading ? "Loading..." : placeholder}</option>
      {customers.map((customer) => (
        <option key={customer.id} value={customer.id}>
          {customer.firstName} {customer.lastName}
        </option>
      ))}
    </select>
  );
}

CustomerSelect.propTypes = {
  value: PropTypes.string,
  onChange: PropTypes.func.isRequired,
  placeholder: PropTypes.string,
};

3. Layout Components (src/components/Layout/)

Application structure components.

// src/components/Layout/Sidebar.jsx
import React from "react";
import { Link, useLocation } from "react-router-dom";
import { HomeIcon, UsersIcon, DocumentIcon } from "@heroicons/react/24/outline";

export function Sidebar() {
  const location = useLocation();

  const navigation = [
    { name: "Dashboard", href: "/dashboard", icon: HomeIcon },
    { name: "Customers", href: "/customers", icon: UsersIcon },
    { name: "Orders", href: "/orders", icon: DocumentIcon },
  ];

  return (
    <aside className="w-64 bg-gray-800 text-white min-h-screen">
      <nav className="p-4 space-y-2">
        {navigation.map((item) => {
          const isActive = location.pathname.startsWith(item.href);
          return (
            <Link
              key={item.name}
              to={item.href}
              className={clsx(
                "flex items-center px-4 py-2 rounded-md",
                isActive
                  ? "bg-gray-900 text-white"
                  : "text-gray-300 hover:bg-gray-700"
              )}
            >
              <item.icon className="w-5 h-5 mr-3" />
              {item.name}
            </Link>
          );
        })}
      </nav>
    </aside>
  );
}

Authentication & Authorization

Authenticated Axios Instance

// src/services/Helpers/AuthenticatedAxios.js
import axios from "axios";
import { camelizeKeys } from "humps";
import { API_ENDPOINTS } from "../Config/APIConfig";
import {
  AUTH_TOKEN_TYPE,
  HTTP_HEADERS,
  HTTP_STATUS,
} from "@constants/Authentication";

export function createAuthenticatedAxios(
  baseURL,
  tokenStorage,
  onUnauthorizedCallback = null
) {
  const accessToken = tokenStorage.getAccessToken();

  const authenticatedAxios = axios.create({
    baseURL: baseURL,
    headers: {
      Authorization: `${AUTH_TOKEN_TYPE.JWT} ${accessToken}`,
      "Content-Type": HTTP_HEADERS.CONTENT_TYPE_JSON,
      Accept: HTTP_HEADERS.ACCEPT_JSON,
    },
  });

  // Add response interceptor for automatic token refresh
  authenticatedAxios.interceptors.response.use(
    (response) => response,
    async (error) => {
      const originalConfig = error.config;

      // Handle 401 unauthorized errors
      if (error.response?.status === HTTP_STATUS.UNAUTHORIZED) {
        const refreshToken = tokenStorage.getRefreshToken();

        if (refreshToken) {
          try {
            // Attempt token refresh
            const refreshResponse = await handleTokenRefresh(
              baseURL,
              refreshToken
            );

            if (refreshResponse?.status === HTTP_STATUS.OK) {
              // Save new tokens
              const newAccessToken = refreshResponse.data.access_token;
              const newRefreshToken = refreshResponse.data.refresh_token;

              tokenStorage.setAccessToken(newAccessToken);
              tokenStorage.setRefreshToken(newRefreshToken);

              // Retry original request with new token
              const retryConfig = {
                ...originalConfig,
                headers: {
                  ...originalConfig.headers,
                  Authorization: `${AUTH_TOKEN_TYPE.JWT} ${newAccessToken}`,
                },
              };

              return authenticatedAxios(retryConfig);
            }
          } catch (refreshError) {
            console.error("Token refresh failed:", refreshError);
            if (onUnauthorizedCallback) {
              onUnauthorizedCallback();
            }
          }
        } else if (onUnauthorizedCallback) {
          onUnauthorizedCallback();
        }
      }

      return Promise.reject(error.response?.data || error);
    }
  );

  return authenticatedAxios;
}

async function handleTokenRefresh(baseURL, refreshToken) {
  const refreshAxios = axios.create({
    baseURL: baseURL,
    headers: {
      "Content-Type": HTTP_HEADERS.CONTENT_TYPE_JSON,
      Accept: HTTP_HEADERS.ACCEPT_JSON,
      Authorization: `${AUTH_TOKEN_TYPE.BEARER} ${refreshToken}`,
    },
  });

  const refreshData = { value: refreshToken };

  return await refreshAxios.post(API_ENDPOINTS.REFRESH_TOKEN, refreshData);
}

Token Storage

// src/services/Storage/TokenStorage.js
import { TOKEN_STORAGE_KEYS } from "@constants/Authentication";

export class TokenStorage {
  /**
   * Gets access token from localStorage
   */
  getAccessToken() {
    return localStorage.getItem(TOKEN_STORAGE_KEYS.ACCESS_TOKEN);
  }

  /**
   * Sets access token in localStorage
   */
  setAccessToken(token) {
    if (token) {
      localStorage.setItem(TOKEN_STORAGE_KEYS.ACCESS_TOKEN, token);
    } else {
      localStorage.removeItem(TOKEN_STORAGE_KEYS.ACCESS_TOKEN);
    }
  }

  /**
   * Gets refresh token from localStorage
   */
  getRefreshToken() {
    return localStorage.getItem(TOKEN_STORAGE_KEYS.REFRESH_TOKEN);
  }

  /**
   * Sets refresh token in localStorage
   */
  setRefreshToken(token) {
    if (token) {
      localStorage.setItem(TOKEN_STORAGE_KEYS.REFRESH_TOKEN, token);
    } else {
      localStorage.removeItem(TOKEN_STORAGE_KEYS.REFRESH_TOKEN);
    }
  }

  /**
   * Clears all tokens
   */
  clearTokens() {
    localStorage.removeItem(TOKEN_STORAGE_KEYS.ACCESS_TOKEN);
    localStorage.removeItem(TOKEN_STORAGE_KEYS.REFRESH_TOKEN);
    localStorage.removeItem(TOKEN_STORAGE_KEYS.USER_DATA);
  }

  /**
   * Checks if user is authenticated
   */
  isAuthenticated() {
    return !!this.getAccessToken();
  }
}

Routing & Navigation

Main Router (src/AppRouter.jsx)

// src/AppRouter.jsx
import React from "react";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { useServices } from "./services/Services";

// Pages
import LoginPage from "./pages/Auth/Login/Page";
import DashboardPage from "./pages/Dashboard/Page";
import CustomerListPage from "./pages/Admin/Customer/List/Page";
import CustomerDetailPage from "./pages/Admin/Customer/Detail/Page";
import CustomerCreatePage from "./pages/Admin/Customer/Create/Page";
import CustomerUpdatePage from "./pages/Admin/Customer/Update/Page";

function AppRouter() {
  return (
    <BrowserRouter>
      <Routes>
        {/* Public routes */}
        <Route path="/login" element={<LoginPage />} />

        {/* Protected routes */}
        <Route
          path="/dashboard"
          element={
            <ProtectedRoute>
              <DashboardPage />
            </ProtectedRoute>
          }
        />

        {/* Customer routes */}
        <Route
          path="/customers"
          element={
            <ProtectedRoute>
              <CustomerListPage />
            </ProtectedRoute>
          }
        />
        <Route
          path="/customers/create"
          element={
            <ProtectedRoute>
              <CustomerCreatePage />
            </ProtectedRoute>
          }
        />
        <Route
          path="/customers/:id"
          element={
            <ProtectedRoute>
              <CustomerDetailPage />
            </ProtectedRoute>
          }
        />
        <Route
          path="/customers/:id/update"
          element={
            <ProtectedRoute>
              <CustomerUpdatePage />
            </ProtectedRoute>
          }
        />

        {/* Default redirect */}
        <Route path="/" element={<Navigate to="/dashboard" replace />} />
      </Routes>
    </BrowserRouter>
  );
}

function ProtectedRoute({ children }) {
  const { authManager } = useServices();

  if (!authManager.isAuthenticated()) {
    return <Navigate to="/login" replace />;
  }

  return children;
}

export default AppRouter;

State Management & Caching

Caching Strategy

The application uses a two-tier caching strategy:

  1. Memory Cache: Fastest, cleared on page reload
  2. localStorage Cache: Persistent across sessions

Example: List Page with Caching

// src/pages/Admin/Customer/List/Page.jsx
import React, { useState, useEffect } from "react";
import { useServices } from "@services/Services";
import { CUSTOMERS_PER_PAGE } from "@constants/Customer";

export default function CustomerListPage() {
  const { customerManager } = useServices();
  const [customers, setCustomers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [page, setPage] = useState(1);
  const [totalCount, setTotalCount] = useState(0);

  useEffect(() => {
    fetchCustomers();
  }, [page]);

  async function fetchCustomers(forceRefresh = false) {
    try {
      setLoading(true);

      const data = await customerManager.getCustomers(
        {
          page: page,
          limit: CUSTOMERS_PER_PAGE,
        },
        handleUnauthorized,
        forceRefresh
      );

      setCustomers(data.results || []);
      setTotalCount(data.count || 0);
    } catch (error) {
      console.error("Failed to fetch customers:", error);
      // Show error toast
    } finally {
      setLoading(false);
    }
  }

  function handleUnauthorized() {
    window.location.href = "/login";
  }

  function handleRefresh() {
    fetchCustomers(true); // Force refresh
  }

  return (
    <div className="container mx-auto p-6">
      <div className="flex justify-between items-center mb-6">
        <h1 className="text-3xl font-bold">Customers</h1>
        <button
          onClick={handleRefresh}
          className="btn-secondary"
        >
          Refresh
        </button>
      </div>

      {loading ? (
        <LoadingSpinner />
      ) : (
        <>
          <CustomerTable customers={customers} />
          <Pagination
            currentPage={page}
            totalCount={totalCount}
            pageSize={CUSTOMERS_PER_PAGE}
            onPageChange={setPage}
          />
        </>
      )}
    </div>
  );
}

Styling & Theming

Tailwind CSS Configuration

// tailwind.config.js
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,jsx}",
  ],
  theme: {
    extend: {
      screens: {
        'xs': '400px',
      },
      colors: {
        primary: {
          50: '#eff6ff',
          100: '#dbeafe',
          500: '#3b82f6',
          600: '#2563eb',
          700: '#1d4ed8',
        },
      },
    },
  },
  plugins: [],
}

Global Styles (src/styles/app.css)

@import "tailwindcss";

/* Base styles */
body {
  @apply bg-gray-50 text-gray-900;
}

/* Utility classes */
.btn {
  @apply px-4 py-2 rounded-md font-medium transition-colors;
}

.btn-primary {
  @apply btn bg-blue-600 text-white hover:bg-blue-700;
}

.btn-secondary {
  @apply btn bg-gray-600 text-white hover:bg-gray-700;
}

.form-input {
  @apply block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm;
  @apply focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent;
}

.form-select {
  @apply form-input;
}

Build Configuration

Vite Configuration (vite.config.js)

// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import path from "path";

export default defineConfig(({ mode }) => ({
  plugins: [
    react(),
    tailwindcss(),
  ],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
      "@components": path.resolve(__dirname, "./src/components"),
      "@pages": path.resolve(__dirname, "./src/pages"),
      "@services": path.resolve(__dirname, "./src/services"),
      "@constants": path.resolve(__dirname, "./src/constants"),
      "@utils": path.resolve(__dirname, "./src/utils"),
      "@styles": path.resolve(__dirname, "./src/styles"),
    },
  },
  server: {
    port: 3000,
    open: true,
    cors: true,
    proxy: {
      "/api": {
        target: process.env.VITE_API_URL || "http://localhost:8080",
        changeOrigin: true,
        secure: false,
      },
    },
  },
  build: {
    outDir: "dist",
    sourcemap: mode === "development" || mode === "staging",
    minify: "terser",
    terserOptions: {
      compress: {
        drop_console: mode === "production" || mode === "staging",
        drop_debugger: true,
      },
    },
    rollupOptions: {
      output: {
        manualChunks: {
          "react-vendor": ["react", "react-dom", "react-router-dom"],
          "ui-vendor": ["@heroicons/react", "clsx"],
          "utils-vendor": ["axios", "luxon", "humps"],
        },
      },
    },
    chunkSizeWarningLimit: 1000,
  },
  optimizeDeps: {
    include: [
      "react",
      "react-dom",
      "react-router-dom",
      "@heroicons/react/24/outline",
      "@heroicons/react/24/solid",
    ],
  },
}));

Complete Setup Guide

Step 1: Initialize Project

# Create new React project with Vite
npm create vite@latest my-app -- --template react

cd my-app

# Install dependencies
npm install react@^19.1.0 react-dom@^19.1.0
npm install react-router-dom@^7.8.0
npm install axios@^1.11.0
npm install luxon@^3.7.1
npm install humps@^2.0.1
npm install @heroicons/react@^2.2.0
npm install clsx@^2.1.1
npm install prop-types@^15.8.1

# Install dev dependencies
npm install -D tailwindcss@^4.1.12
npm install -D @tailwindcss/vite@^4.1.11
npm install -D @tailwindcss/postcss@^4.1.12
npm install -D vite@^7.1.2
npm install -D @vitejs/plugin-react@^4.7.0
npm install -D terser@^5.44.0
npm install -D eslint@^9.30.1
npm install -D @eslint/js@^9.30.1
npm install -D eslint-plugin-react-hooks@^5.2.0

Step 2: Configure Vite

Create vite.config.js as shown in Build Configuration section.

Step 3: Configure Tailwind

Create tailwind.config.js as shown in Styling section.

Step 4: Create Directory Structure

mkdir -p src/{components/{UI,UIX,business,Layout},pages,services/{API,Storage,Manager,Config,Helpers},constants,utils,styles,assets}

Step 5: Create Service Layer

  1. Create API Config
// src/services/Config/APIConfig.js
export function getAPIBaseURL() {
  return import.meta.env.VITE_API_URL || "http://localhost:8080";
}

export const API_ENDPOINTS = {
  // Auth
  LOGIN: "/api/v1/auth/login",
  LOGOUT: "/api/v1/auth/logout",
  REFRESH_TOKEN: "/api/v1/auth/refresh",

  // Customers
  CUSTOMERS: "/api/v1/customers",
  CUSTOMER_DETAIL: "/api/v1/customers/{id}",
  CUSTOMER_COUNT: "/api/v1/customers/count",
};

export const ENV_CONFIG = {
  API_URL: import.meta.env.VITE_API_URL,
  ENV: import.meta.env.MODE,
};
  1. Create Storage Services (TokenStorage, CustomerStorage, etc.)
  2. Create API Services (AuthAPI, CustomerAPI, etc.)
  3. Create Manager Services (AuthManager, CustomerManager, etc.)
  4. Create Services Container (src/services/Services.jsx)

Step 6: Create Constants

Create constant files for each domain (Customer, Auth, UI, etc.) as shown in Constants Management section.

Step 7: Create Components

  1. Create UI components (Button, Input, Modal, etc.)
  2. Create Layout components (Sidebar, Header, Footer)
  3. Create business components (CustomerSelect, etc.)

Step 8: Create Pages

Create page components following the structure:

pages/
└── Admin/
    └── Customer/
        ├── List/
        │   └── Page.jsx
        ├── Detail/
        │   └── Page.jsx
        ├── Create/
        │   └── Page.jsx
        └── Update/
            └── Page.jsx

Step 9: Setup Router

Create AppRouter.jsx as shown in Routing section.

Step 10: Setup Main Entry

// src/main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./AppRouter";
import { ServiceProvider } from "./services/Services";
import "./styles/app.css";

// Error Boundary
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

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

  componentDidCatch(error, errorInfo) {
    console.error("Error:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="min-h-screen flex items-center justify-center">
          <div className="bg-white p-6 rounded-lg shadow-lg">
            <h1 className="text-2xl font-bold text-red-600 mb-4">
              Something went wrong
            </h1>
            <button
              onClick={() => window.location.reload()}
              className="btn-primary"
            >
              Reload Page
            </button>
          </div>
        </div>
      );
    }

    return this.props.children;
  }
}

const root = ReactDOM.createRoot(document.getElementById("root"));

root.render(
  <React.StrictMode>
    <ErrorBoundary>
      <ServiceProvider>
        <App />
      </ServiceProvider>
    </ErrorBoundary>
  </React.StrictMode>
);

Step 11: Environment Variables

Create .env files:

# .env.development
VITE_API_URL=http://localhost:8080

# .env.production
VITE_API_URL=https://api.production.com

# .env.staging
VITE_API_URL=https://api.staging.com

Step 12: Package.json Scripts

{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "build:production": "vite build --mode production",
    "build:staging": "vite build --mode staging",
    "preview": "vite preview",
    "lint": "eslint .",
    "clean": "rm -rf node_modules dist",
    "clean:install": "npm run clean && npm install"
  }
}

Best Practices & Patterns

1. Service Layer Rules

  • Components ONLY interact with Manager services
  • Never call API services directly from components
  • Never access Storage services directly from components
  • Managers orchestrate API and Storage layers

2. Constants Rules

  • ZERO TOLERANCE for magic values
  • Every number, string, or config value must be a constant
  • Check src/constants/ before writing ANY code
  • Create new constants if they don't exist

3. Component Rules

  • UI components are pure and stateless
  • Business components use the service layer
  • Page components handle routing logic
  • Always use PropTypes

4. Error Handling

// Consistent error handling pattern
try {
  const data = await manager.getData();
  setData(data);
} catch (error) {
  console.error("Operation failed:", error);
  // Show user-friendly error message
  showToast("Failed to load data", "error");
} finally {
  setLoading(false);
}

5. Caching Strategy

  • Use memory cache first (fastest)
  • Fall back to localStorage
  • Clear cache on mutations
  • Implement loading flags to prevent duplicate requests

6. Code Organization

// Component structure order:
// 1. Imports
// 2. Component definition
// 3. State hooks
// 4. Effect hooks
// 5. Event handlers
// 6. Helper functions
// 7. Render logic
// 8. PropTypes

7. Naming Conventions

  • Components: PascalCase (CustomerList)
  • Functions: camelCase (fetchCustomers)
  • Constants: UPPER_SNAKE_CASE (CUSTOMER_STATUS)
  • CSS Classes: kebab-case (customer-list)
  • Files: PascalCase for components, camelCase for utilities

8. Testing Checklist

Before deployment, verify:

  • All magic values replaced with constants
  • Components use Manager services only
  • Error handling implemented
  • Loading states handled
  • Caching working correctly
  • Authentication flow tested
  • Token refresh working
  • PropTypes defined
  • Console logs removed (in production)
  • Build succeeds without warnings

Conclusion

This architecture provides:

  • Scalability: Clear separation of concerns allows teams to work independently
  • Maintainability: Consistent patterns make code easy to understand and modify
  • Testability: Service layer isolation enables comprehensive testing
  • Performance: Multi-tier caching reduces API calls
  • Reliability: Automatic token refresh and error handling
  • Developer Experience: Path aliases, constants, and clear patterns

Follow these patterns strictly to maintain consistency and quality across the entire application.


Document Version: 1.0.0 Last Updated: 2025-01-30 Maintained By: Development Team