Front-end Engineering Lab
PatternsArchitecture Patterns

Adapter Pattern

Transform incompatible interfaces into compatible ones - commonly used for server schema normalization and API versioning

The Adapter Pattern allows objects with incompatible interfaces to work together. In front-end development, it's commonly used for server schema normalization, API versioning, and transforming data between different formats.

🎯 The Problem

// ❌ BAD: Direct usage of incompatible server schema
interface ServerUser {
  user_id: number;
  full_name: string;
  email_address: string;
  created_at: string;
}

function UserProfile({ serverUser }: { serverUser: ServerUser }) {
  // Inconsistent naming, different structure
  return (
    <div>
      <h1>{serverUser.full_name}</h1>
      <p>{serverUser.email_address}</p>
      <span>ID: {serverUser.user_id}</span>
    </div>
  );
}

// ✅ GOOD: Adapter Pattern
interface ClientUser {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
}

class UserAdapter {
  static fromServer(serverUser: ServerUser): ClientUser {
    return {
      id: serverUser.user_id,
      name: serverUser.full_name,
      email: serverUser.email_address,
      createdAt: new Date(serverUser.created_at)
    };
  }
  
  static toServer(clientUser: ClientUser): ServerUser {
    return {
      user_id: clientUser.id,
      full_name: clientUser.name,
      email_address: clientUser.email,
      created_at: clientUser.createdAt.toISOString()
    };
  }
}

function UserProfile({ serverUser }: { serverUser: ServerUser }) {
  const user = UserAdapter.fromServer(serverUser);
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <span>ID: {user.id}</span>
    </div>
  );
}

📚 Common Front-End Examples

1. Server Schema Normalization

// Server API returns different schema
interface ServerProduct {
  product_id: string;
  product_name: string;
  price_cents: number;
  in_stock: boolean;
  category: {
    category_id: number;
    category_name: string;
  };
  images: Array<{
    image_url: string;
    image_alt: string;
  }>;
}

// Client expects normalized schema
interface ClientProduct {
  id: string;
  name: string;
  price: number;
  available: boolean;
  category: {
    id: number;
    name: string;
  };
  images: Array<{
    url: string;
    alt: string;
  }>;
}

class ProductAdapter {
  static fromServer(serverProduct: ServerProduct): ClientProduct {
    return {
      id: serverProduct.product_id,
      name: serverProduct.product_name,
      price: serverProduct.price_cents / 100, // Convert cents to dollars
      available: serverProduct.in_stock,
      category: {
        id: serverProduct.category.category_id,
        name: serverProduct.category.category_name
      },
      images: serverProduct.images.map(img => ({
        url: img.image_url,
        alt: img.image_alt
      }))
    };
  }
  
  static fromServerArray(serverProducts: ServerProduct[]): ClientProduct[] {
    return serverProducts.map(ProductAdapter.fromServer);
  }
  
  static toServer(clientProduct: ClientProduct): ServerProduct {
    return {
      product_id: clientProduct.id,
      product_name: clientProduct.name,
      price_cents: Math.round(clientProduct.price * 100),
      in_stock: clientProduct.available,
      category: {
        category_id: clientProduct.category.id,
        category_name: clientProduct.category.name
      },
      images: clientProduct.images.map(img => ({
        image_url: img.url,
        image_alt: img.alt
      }))
    };
  }
}

// Usage
import { useState, useEffect } from 'react';

async function fetchProducts(): Promise<ClientProduct[]> {
  const response = await fetch('/api/products');
  const serverProducts: ServerProduct[] = await response.json();
  return ProductAdapter.fromServerArray(serverProducts);
}

function ProductList() {
  const [products, setProducts] = useState<ClientProduct[]>([]);
  
  useEffect(() => {
    fetchProducts().then(setProducts);
  }, []);
  
  return (
    <div>
      {products.map(product => (
        <div key={product.id}>
          <h2>{product.name}</h2>
          <p>${product.price}</p>
          <span>{product.available ? 'In Stock' : 'Out of Stock'}</span>
        </div>
      ))}
    </div>
  );
}

2. API Versioning Adapter

// Old API v1
interface ApiV1Response {
  data: {
    users: Array<{
      id: number;
      name: string;
    }>;
  };
}

// New API v2
interface ApiV2Response {
  users: Array<{
    userId: number;
    userName: string;
    metadata: {
      createdAt: string;
    };
  }>;
}

// Unified client interface
interface UnifiedUser {
  id: number;
  name: string;
  createdAt?: Date;
}

class ApiVersionAdapter {
  static fromV1(response: ApiV1Response): UnifiedUser[] {
    return response.data.users.map(user => ({
      id: user.id,
      name: user.name
    }));
  }
  
  static fromV2(response: ApiV2Response): UnifiedUser[] {
    return response.users.map(user => ({
      id: user.userId,
      name: user.userName,
      createdAt: new Date(user.metadata.createdAt)
    }));
  }
}

// Usage
import { useState, useEffect } from 'react';

async function fetchUsers(version: 'v1' | 'v2'): Promise<UnifiedUser[]> {
  const response = await fetch(`/api/${version}/users`);
  
  if (version === 'v1') {
    const data: ApiV1Response = await response.json();
    return ApiVersionAdapter.fromV1(data);
  } else {
    const data: ApiV2Response = await response.json();
    return ApiVersionAdapter.fromV2(data);
  }
}

function UserList() {
  const [users, setUsers] = useState<UnifiedUser[]>([]);
  
  useEffect(() => {
    fetchUsers('v2').then(setUsers);
  }, []);
  
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          {user.name}
          {user.createdAt && (
            <span> - Joined: {user.createdAt.toLocaleDateString()}</span>
          )}
        </li>
      ))}
    </ul>
  );
}

3. Third-Party Library Adapter

// Third-party library interface
interface ThirdPartyChart {
  render(data: {
    labels: string[];
    values: number[];
  }): void;
}

// Our application interface
interface ChartData {
  name: string;
  value: number;
}

class ChartAdapter {
  private chart: ThirdPartyChart;
  
  constructor(chart: ThirdPartyChart) {
    this.chart = chart;
  }
  
  render(data: ChartData[]): void {
    const adapted = {
      labels: data.map(d => d.name),
      values: data.map(d => d.value)
    };
    this.chart.render(adapted);
  }
}

// Usage
import { useRef, useState, useEffect } from 'react';

function Dashboard() {
  const chartRef = useRef<ThirdPartyChart | null>(null);
  const [data, setData] = useState<ChartData[]>([]);
  
  useEffect(() => {
    if (chartRef.current) {
      const adapter = new ChartAdapter(chartRef.current);
      adapter.render(data);
    }
  }, [data]);
  
  return <div ref={chartRef} />;
}

4. Form Data Adapter

// Server expects form data format
interface ServerFormData {
  fields: Array<{
    field_name: string;
    field_value: string;
  }>;
}

// Client uses object format
interface ClientFormData {
  [key: string]: string;
}

class FormDataAdapter {
  static toServer(clientData: ClientFormData): ServerFormData {
    return {
      fields: Object.entries(clientData).map(([name, value]) => ({
        field_name: name,
        field_value: value
      }))
    };
  }
  
  static fromServer(serverData: ServerFormData): ClientFormData {
    const result: ClientFormData = {};
    serverData.fields.forEach(field => {
      result[field.field_name] = field.field_value;
    });
    return result;
  }
}

// Usage
import { useState } from 'react';

function ContactForm() {
  const [formData, setFormData] = useState<ClientFormData>({
    name: '',
    email: '',
    message: ''
  });
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    const serverData = FormDataAdapter.toServer(formData);
    await fetch('/api/contact', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(serverData)
    });
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        value={formData.name}
        onChange={(e) => setFormData({ ...formData, name: e.target.value })}
        placeholder="Name"
      />
      <input
        type="email"
        value={formData.email}
        onChange={(e) => setFormData({ ...formData, email: e.target.value })}
        placeholder="Email"
      />
      <textarea
        value={formData.message}
        onChange={(e) => setFormData({ ...formData, message: e.target.value })}
        placeholder="Message"
      />
      <button type="submit">Submit</button>
    </form>
  );
}

5. Date/Time Format Adapter

// Server returns ISO strings
interface ServerEvent {
  event_id: string;
  event_name: string;
  start_time: string; // ISO string
  end_time: string; // ISO string
  timezone: string;
}

// Client uses Date objects
interface ClientEvent {
  id: string;
  name: string;
  startTime: Date;
  endTime: Date;
  timezone: string;
}

class DateTimeAdapter {
  static fromServer(serverEvent: ServerEvent): ClientEvent {
    return {
      id: serverEvent.event_id,
      name: serverEvent.event_name,
      startTime: new Date(serverEvent.start_time),
      endTime: new Date(serverEvent.end_time),
      timezone: serverEvent.timezone
    };
  }
  
  static toServer(clientEvent: ClientEvent): ServerEvent {
    return {
      event_id: clientEvent.id,
      event_name: clientEvent.name,
      start_time: clientEvent.startTime.toISOString(),
      end_time: clientEvent.endTime.toISOString(),
      timezone: clientEvent.timezone
    };
  }
}

// Usage
import { useState, useEffect } from 'react';

function EventCalendar() {
  const [events, setEvents] = useState<ClientEvent[]>([]);
  
  useEffect(() => {
    async function loadEvents() {
      const response = await fetch('/api/events');
      const serverEvents: ServerEvent[] = await response.json();
      const clientEvents = serverEvents.map(DateTimeAdapter.fromServer);
      setEvents(clientEvents);
    }
    loadEvents();
  }, []);
  
  return (
    <div>
      {events.map(event => (
        <div key={event.id}>
          <h3>{event.name}</h3>
          <p>Start: {event.startTime.toLocaleString()}</p>
          <p>End: {event.endTime.toLocaleString()}</p>
        </div>
      ))}
    </div>
  );
}

🎯 When to Use

This pattern is commonly used and recommended for:

  1. Server schema normalization - Transform server data to match client expectations
  2. API versioning - Support multiple API versions with unified interface
  3. Third-party integrations - Adapt external library interfaces to your needs
  4. Data format conversion - Transform between different data formats (JSON, XML, etc.)
  5. Legacy system integration - Integrate with old systems without changing client code

📚 Key Takeaways

  • Decouples interfaces - Client code doesn't depend on server schema
  • Single responsibility - Adapter handles transformation logic
  • Maintainability - Changes to server schema only affect adapter
  • Testability - Easy to test adapter in isolation
  • Flexibility - Support multiple data sources with same client interface

On this page