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:
- Server schema normalization - Transform server data to match client expectations
- API versioning - Support multiple API versions with unified interface
- Third-party integrations - Adapt external library interfaces to your needs
- Data format conversion - Transform between different data formats (JSON, XML, etc.)
- 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