Skip to content

Latest commit

 

History

History
347 lines (276 loc) · 8.11 KB

File metadata and controls

347 lines (276 loc) · 8.11 KB

Architecture Decisions

This document explains why we made specific architectural choices.

Table of Contents

  1. Project Structure
  2. State Management
  3. Styling
  4. API Layer
  5. Code Patterns

Project Structure

Feature-Based vs Type-Based

We use feature-based structure:

src/features/
├── auth/
│   ├── components/
│   ├── hooks/
│   ├── services/
│   └── slices/
└── products/
    ├── components/
    ├── hooks/
    ├── services/
    └── slices/

Why feature-based?

Feature-Based Type-Based
Related code lives together All components in one folder
Easy to find related files Hard to navigate as app grows
Enables code splitting Everything loads together
Scales well Becomes messy

When to use type-based:

  • Very small apps (< 5 pages)
  • Team prefers that structure

State Management

Context API (Primary) vs Redux Toolkit

Aspect Context API Redux Toolkit
Performance Good for low-frequency updates Optimized, memoized
DevTools Limited Time-travel debugging
Boilerplate Minimal Minimal (thanks to RTK)
Scalability Good for small-medium apps Excellent
Learning curve Low Medium

Why Context API?

// AuthContext.jsx - Simple and effective
export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [token, setToken] = useState(() => localStorage.getItem('token'));

  const login = async (credentials) => {
    const response = await authService.login(credentials);
    setToken(response.data.accessToken);
    setUser(response.data.user);
    localStorage.setItem('token', response.data.accessToken);
  };

  const logout = () => {
    setToken(null);
    setUser(null);
    localStorage.removeItem('token');
  };

  return (
    <AuthContext.Provider value={{ user, token, isAuthenticated: !!user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

When to use Context:

  • Auth state (user, login, logout)
  • Theme (light/dark mode)
  • Locale/i18n
  • Simple UI state (sidebar open)
  • Feature-level state (products list)

Why not Redux?

  • Redux adds unnecessary complexity for simple state management
  • Context API is built into React (no extra dependencies)
  • Easier to understand for beginners
  • Suitable for the scale of bootcamp projects

Note: Redux Toolkit is still available in the boilerplate but is not used by default. Use it only if you have complex cross-component state that Context cannot handle efficiently.


Styling

Tailwind CSS vs Styled Components

Aspect Tailwind CSS Styled Components
Learning curve Medium Low
Performance Excellent (purged) Good
No class conflicts Yes Yes
JS/CSS separation No Yes
IDE support Good Excellent

Why Tailwind CSS?

  1. Faster prototyping

    // Tailwind
    <div className="p-4 bg-white rounded-lg shadow">
    
    // Styled Components
    const Container = styled.div`
      padding: 1rem;
      background: white;
      border-radius: 0.5rem;
      box-shadow: ...
    `;
  2. Smaller bundle - Unused styles removed

  3. Easier maintenance - No file hopping

  4. Consistency - Same spacing, colors


API Layer

Axios with Interceptors

// services/api.js
const api = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
});

// Request interceptor - add token
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Response interceptor - handle errors
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      localStorage.removeItem('token');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

Why this approach?

  • Centralized - All API calls go through one instance
  • Token management - Automatically added to every request
  • Error handling - Global error handling
  • Easy testing - Can mock the api instance

Code Patterns

Custom Hook Pattern

// context/AuthContext.jsx
export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [token, setToken] = useState(() => localStorage.getItem('token'));

  const login = async (credentials) => {
    const response = await authService.login(credentials);
    setToken(response.data.accessToken);
    setUser(response.data.user);
  };

  const logout = () => {
    setToken(null);
    setUser(null);
    localStorage.removeItem('token');
  };

  return (
    <AuthContext.Provider value={{ user, token, isAuthenticated: !!token, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

// hooks/useAuthContext.js
export const useAuthContext = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuthContext must be used within an AuthProvider');
  }
  return context;
};

// hooks/useAuth.js (wrapper for convenience)
import { useAuthContext } from '../context/AuthContext';

export const useAuth = () => {
  return useAuthContext();
};

Why this pattern?

  • Reusable logic
  • Separation of concerns
  • Easy to test
  • Cleaner components
  • Native React (no extra dependencies)

Context + Service Pattern for CRUD

// context/ProductContext.jsx
export function ProductProvider({ children }) {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(false);

  const fetchProducts = async () => {
    setLoading(true);
    try {
      const response = await productService.getAll();
      setProducts(response.data);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const addProduct = async (data) => {
    const response = await productService.create(data);
    setProducts(prev => [...prev, response.data]);
    return response.data;
  };

  return (
    <ProductContext.Provider value={{ products, loading, fetchProducts, addProduct }}>
      {children}
    </ProductContext.Provider>
  );
}

Why this pattern?

  • State lives close to where it's used
  • No global store to manage
  • Simple data flow
  • Easy to understand

File Naming Conventions

Type Convention Example
Components PascalCase LoginForm.jsx
Hooks camelCase + use prefix useAuth.js
Services camelCase + Service suffix authService.js
Slices camelCase + Slice suffix authSlice.js
Utils camelCase validators.js
Constants SCREAMING_SNAKE constants.js

Component Structure

// components/ui/Button.jsx
import PropTypes from 'prop-types';

export function Button({ 
  children, 
  variant = 'primary', 
  onClick,
  disabled 
}) {
  const baseStyles = 'px-4 py-2 rounded font-medium transition';
  const variants = {
    primary: 'bg-blue-500 text-white hover:bg-blue-600',
    secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
  };

  return (
    <button
      className={`${baseStyles} ${variants[variant]}`}
      onClick={onClick}
      disabled={disabled}
    >
      {children}
    </button>
  );
}

Button.propTypes = {
  children: PropTypes.node.isRequired,
  variant: PropTypes.oneOf(['primary', 'secondary']),
  onClick: PropTypes.func,
  disabled: PropTypes.bool,
};

Why this structure?

  • Props documentation with PropTypes
  • Reusable style variants
  • Clear prop naming
  • Type-safe (PropTypes)

Summary

Decision Rationale
Feature-based structure Scalability, code splitting
Context API Native React, simplicity, no extra dependencies
Tailwind CSS Speed, consistency, bundle size
Axios interceptors Centralized auth/error handling
Custom hooks Reusability, clean code
AI Integration Context-based for easy state management