Docs For AI
Architecture

Design Patterns

Common design patterns for frontend development

Design Patterns

Design patterns provide proven solutions to common problems. This covers patterns frequently used in frontend development.

Module Pattern

ES Modules

// math.ts - clean exports
export const PI = 3.14159;

export function add(a: number, b: number): number {
  return a + b;
}

export function multiply(a: number, b: number): number {
  return a * b;
}

// Default export for main functionality
export default class Calculator {
  // ...
}

// usage.ts
import Calculator, { add, multiply, PI } from './math';
import * as math from './math';

Revealing Module Pattern

// Private implementation, public interface
function createCounter() {
  // Private
  let count = 0;
  const MAX_COUNT = 100;

  function validateCount(n: number): boolean {
    return n >= 0 && n <= MAX_COUNT;
  }

  // Public API
  return {
    getCount: () => count,
    increment: () => {
      if (validateCount(count + 1)) {
        count++;
      }
      return count;
    },
    decrement: () => {
      if (validateCount(count - 1)) {
        count--;
      }
      return count;
    },
    reset: () => {
      count = 0;
      return count;
    },
  };
}

const counter = createCounter();
counter.increment(); // 1
counter.getCount();  // 1

Singleton Pattern

// Class-based singleton
class Logger {
  private static instance: Logger;
  private logs: string[] = [];

  private constructor() {}

  static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }

  log(message: string): void {
    this.logs.push(`[${new Date().toISOString()}] ${message}`);
    console.log(message);
  }

  getLogs(): string[] {
    return [...this.logs];
  }
}

// Usage
const logger = Logger.getInstance();
logger.log('Application started');

// Module singleton (simpler)
// logger.ts
class LoggerService {
  private logs: string[] = [];

  log(message: string): void {
    this.logs.push(message);
  }
}

export const logger = new LoggerService();

Factory Pattern

// Component factory
interface NotificationProps {
  message: string;
  type: 'success' | 'error' | 'warning' | 'info';
}

function createNotification({ message, type }: NotificationProps) {
  const config = {
    success: { icon: CheckIcon, color: 'green' },
    error: { icon: XIcon, color: 'red' },
    warning: { icon: AlertIcon, color: 'yellow' },
    info: { icon: InfoIcon, color: 'blue' },
  };

  const { icon: Icon, color } = config[type];

  return (
    <div className={`notification notification-${color}`}>
      <Icon />
      <span>{message}</span>
    </div>
  );
}

// API client factory
interface ApiClientConfig {
  baseUrl: string;
  timeout?: number;
  headers?: Record<string, string>;
}

function createApiClient(config: ApiClientConfig) {
  const { baseUrl, timeout = 5000, headers = {} } = config;

  async function request<T>(
    endpoint: string,
    options: RequestInit = {}
  ): Promise<T> {
    const response = await fetch(`${baseUrl}${endpoint}`, {
      ...options,
      headers: { ...headers, ...options.headers },
      signal: AbortSignal.timeout(timeout),
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    return response.json();
  }

  return {
    get: <T>(endpoint: string) => request<T>(endpoint),
    post: <T>(endpoint: string, data: unknown) =>
      request<T>(endpoint, {
        method: 'POST',
        body: JSON.stringify(data),
        headers: { 'Content-Type': 'application/json' },
      }),
    put: <T>(endpoint: string, data: unknown) =>
      request<T>(endpoint, {
        method: 'PUT',
        body: JSON.stringify(data),
        headers: { 'Content-Type': 'application/json' },
      }),
    delete: <T>(endpoint: string) =>
      request<T>(endpoint, { method: 'DELETE' }),
  };
}

// Usage
const api = createApiClient({ baseUrl: 'https://api.example.com' });
const users = await api.get<User[]>('/users');

Observer Pattern

// Event emitter
type Listener<T> = (data: T) => void;

class EventEmitter<Events extends Record<string, any>> {
  private listeners = new Map<keyof Events, Set<Listener<any>>>();

  on<K extends keyof Events>(event: K, listener: Listener<Events[K]>): () => void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(listener);

    // Return unsubscribe function
    return () => this.off(event, listener);
  }

  off<K extends keyof Events>(event: K, listener: Listener<Events[K]>): void {
    this.listeners.get(event)?.delete(listener);
  }

  emit<K extends keyof Events>(event: K, data: Events[K]): void {
    this.listeners.get(event)?.forEach(listener => listener(data));
  }
}

// Type-safe events
interface AppEvents {
  userLogin: { userId: string; timestamp: Date };
  userLogout: { userId: string };
  notification: { message: string; type: 'success' | 'error' };
}

const events = new EventEmitter<AppEvents>();

// Subscribe
const unsubscribe = events.on('userLogin', ({ userId, timestamp }) => {
  console.log(`User ${userId} logged in at ${timestamp}`);
});

// Emit
events.emit('userLogin', { userId: '123', timestamp: new Date() });

// Unsubscribe
unsubscribe();

React Hook for Events

function useEvent<T>(
  emitter: EventEmitter<any>,
  event: string,
  handler: (data: T) => void
) {
  useEffect(() => {
    return emitter.on(event, handler);
  }, [emitter, event, handler]);
}

// Usage
function NotificationListener() {
  useEvent(events, 'notification', ({ message, type }) => {
    showToast(message, type);
  });

  return null;
}

Strategy Pattern

// Validation strategies
interface ValidationStrategy {
  validate(value: string): { valid: boolean; message?: string };
}

const emailStrategy: ValidationStrategy = {
  validate(value) {
    const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
    return { valid, message: valid ? undefined : 'Invalid email format' };
  },
};

const passwordStrategy: ValidationStrategy = {
  validate(value) {
    const valid = value.length >= 8 && /[A-Z]/.test(value) && /\d/.test(value);
    return {
      valid,
      message: valid ? undefined : 'Password must be 8+ chars with uppercase and number',
    };
  },
};

const phoneStrategy: ValidationStrategy = {
  validate(value) {
    const valid = /^\+?[\d\s-]{10,}$/.test(value);
    return { valid, message: valid ? undefined : 'Invalid phone number' };
  },
};

// Validator using strategies
class FormValidator {
  private strategies = new Map<string, ValidationStrategy>();

  addStrategy(field: string, strategy: ValidationStrategy) {
    this.strategies.set(field, strategy);
  }

  validate(field: string, value: string) {
    const strategy = this.strategies.get(field);
    if (!strategy) return { valid: true };
    return strategy.validate(value);
  }

  validateAll(data: Record<string, string>) {
    const errors: Record<string, string> = {};
    let isValid = true;

    for (const [field, value] of Object.entries(data)) {
      const result = this.validate(field, value);
      if (!result.valid) {
        isValid = false;
        errors[field] = result.message!;
      }
    }

    return { isValid, errors };
  }
}

// Usage
const validator = new FormValidator();
validator.addStrategy('email', emailStrategy);
validator.addStrategy('password', passwordStrategy);

const result = validator.validateAll({
  email: 'test@example.com',
  password: 'weak',
});
// { isValid: false, errors: { password: '...' } }

Decorator Pattern

// Function decorator
function withLogging<T extends (...args: any[]) => any>(fn: T): T {
  return ((...args: Parameters<T>) => {
    console.log(`Calling ${fn.name} with`, args);
    const result = fn(...args);
    console.log(`${fn.name} returned`, result);
    return result;
  }) as T;
}

const add = (a: number, b: number) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(1, 2); // Logs: Calling add with [1, 2], add returned 3

// HOC as decorator (React)
function withAuth<P extends object>(
  WrappedComponent: React.ComponentType<P>
) {
  return function AuthenticatedComponent(props: P) {
    const { isAuthenticated, isLoading } = useAuth();

    if (isLoading) return <LoadingSpinner />;
    if (!isAuthenticated) return <Navigate to="/login" />;

    return <WrappedComponent {...props} />;
  };
}

const ProtectedDashboard = withAuth(Dashboard);

// TypeScript decorators (experimental)
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey}`);
    return originalMethod.apply(this, args);
  };

  return descriptor;
}

class UserService {
  @log
  async getUser(id: string) {
    // ...
  }
}

Proxy Pattern

// Reactive proxy (like Vue 3)
function reactive<T extends object>(target: T): T {
  const handlers: ProxyHandler<T> = {
    get(target, prop, receiver) {
      const value = Reflect.get(target, prop, receiver);
      track(target, prop); // Track dependency
      return typeof value === 'object' ? reactive(value) : value;
    },
    set(target, prop, value, receiver) {
      const result = Reflect.set(target, prop, value, receiver);
      trigger(target, prop); // Trigger updates
      return result;
    },
  };

  return new Proxy(target, handlers);
}

// Lazy loading proxy
function createLazyProxy<T extends object>(
  loader: () => Promise<T>
): T {
  let instance: T | null = null;
  let loading: Promise<T> | null = null;

  return new Proxy({} as T, {
    get(_, prop) {
      if (!instance) {
        if (!loading) {
          loading = loader().then(result => {
            instance = result;
            return result;
          });
        }
        throw loading; // For Suspense
      }
      return instance[prop as keyof T];
    },
  });
}

Mediator Pattern

// Central mediator for component communication
class ComponentMediator {
  private components = new Map<string, any>();

  register(name: string, component: any) {
    this.components.set(name, component);
  }

  unregister(name: string) {
    this.components.delete(name);
  }

  send(from: string, to: string, message: any) {
    const target = this.components.get(to);
    if (target?.receive) {
      target.receive(from, message);
    }
  }

  broadcast(from: string, message: any) {
    this.components.forEach((component, name) => {
      if (name !== from && component.receive) {
        component.receive(from, message);
      }
    });
  }
}

// Usage with React context
const MediatorContext = createContext<ComponentMediator | null>(null);

function useMediator(name: string, handler: (from: string, msg: any) => void) {
  const mediator = useContext(MediatorContext);

  useEffect(() => {
    const component = { receive: handler };
    mediator?.register(name, component);
    return () => mediator?.unregister(name);
  }, [mediator, name, handler]);

  return {
    send: (to: string, message: any) => mediator?.send(name, to, message),
    broadcast: (message: any) => mediator?.broadcast(name, message),
  };
}

Best Practices

Design Pattern Guidelines

  1. Don't force patterns - use when they solve a real problem
  2. Prefer composition over inheritance
  3. Keep patterns simple and maintainable
  4. Document pattern usage for team understanding
  5. Consider React/Vue built-in patterns first
  6. Test pattern implementations thoroughly
  7. Refactor to patterns when complexity warrants it

On this page