Learn how the adapter design pattern connects incompatible interfaces. This guide covers TypeScript, React, and Node.js with real-world, practical examples.
December 19, 2025 (1d ago)
A Guide to the Adapter Design Pattern in Modern Code
Learn how the adapter design pattern connects incompatible interfaces. This guide covers TypeScript, React, and Node.js with real-world, practical examples.
← Back to blog
A Guide to the Adapter Design Pattern in Modern Code
Summary: Learn how the adapter design pattern connects incompatible interfaces. This guide covers TypeScript, React, and Node.js with practical examples.
Introduction
Ever had a perfectly good piece of code — a third-party library or a legacy module — that simply won’t fit with the rest of your system? It’s like using a European plug with a North American socket. They both work, but their interfaces are incompatible.
The Adapter pattern solves that problem. It translates one interface into another so existing code can be reused without modifying the original implementation. This article explains the pattern, shows TypeScript and React examples, and demonstrates a Node.js adapter that modernizes callback-based modules.
Why the Adapter Pattern Matters
The Adapter pattern is a structural pattern that wraps an incompatible object and exposes an interface your code expects. It was first documented by the Gang of Four in 19941, and it remains a practical tool for integrating libraries, modernizing legacy code, and unifying multiple data sources. For a focused explanation and examples, see the Adapter pattern overview2.
Common situations where adapters help:
- Integrating third-party APIs that return different shapes of data.
- Modernizing legacy code that uses callbacks so it fits with async/await.
- Unifying multiple data sources into a single interface for UI components.
Using an adapter keeps business logic clean and decoupled from external systems.
Adapter Pattern at a Glance
| Concept | Description |
|---|---|
| Type | Structural |
| Primary intent | Allow objects with incompatible interfaces to work together |
| Core idea | Wrap the adaptee to expose the target interface |
| Key problem solved | Reuse existing classes without changing their source code |
| Common use cases | Third-party libraries, legacy code, multiple data sources |
Structure and Roles
The pattern has four main roles:
- The Client — the code that needs a specific interface.
- The Target Interface — the contract the Client expects.
- The Adaptee — the incompatible class or module that has the needed functionality.
- The Adapter — implements the Target and delegates to the Adaptee, translating calls as needed.
Two common adapter styles:
- Object adapter (composition): Adapter holds an instance of the Adaptee. This is the most flexible approach.
- Class adapter (inheritance): Adapter inherits from Adaptee and implements Target. This requires multiple inheritance and is less common in modern JavaScript and TypeScript.
Practical Example: TypeScript + React
Imagine a dashboard that receives user profiles from two services with different response shapes. Without an adapter, your components would be littered with conditional logic.
Incompatible API shapes
// Data from UserServiceA
interface UserA {
userId: number;
fullName: string;
emailAddress: string;
}
// Data from UserServiceB
interface UserB {
id: string;
name: string;
contact: {
email: string;
};
}
Target interface our app expects
interface UnifiedUser {
id: string;
name: string;
email: string;
}
TypeScript adapters
// Adapter for UserServiceA
function adaptUserA(userA: UserA): UnifiedUser {
return {
id: userA.userId.toString(),
name: userA.fullName,
email: userA.emailAddress,
};
}
// Adapter for UserServiceB
function adaptUserB(userB: UserB): UnifiedUser {
return {
id: userB.id,
name: userB.name,
email: userB.contact.email,
};
}
By centralizing transformations, components stay clean and resilient. If an API renames a field, only the adapter changes.
React component that consumes unified data
interface UserProfileProps {
user: UnifiedUser;
}
const UserProfile: React.FC<UserProfileProps> = ({ user }) => {
return (
<div>
<h2>{user.name}</h2>
<p>ID: {user.id}</p>
<p>Email: {user.email}</p>
</div>
);
};
This component relies on a single, predictable shape. That makes testing and reuse straightforward.
Example: Modernizing a Callback-Based Node.js Module
Legacy modules often use error-first callbacks. Rather than changing a stable, battle-tested module, build an adapter that exposes a Promise-based API.
Legacy adaptee (do not modify)
// legacyFileProcessor.js
const fs = require('fs');
class LegacyFileProcessor {
processFile(filePath, callback) {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
return callback(err, null);
}
const processedContent = data.toUpperCase();
callback(null, processedContent);
});
}
}
module.exports = LegacyFileProcessor;
Adapter that returns a Promise
// FileProcessorAdapter.js
const LegacyFileProcessor = require('./legacyFileProcessor');
class FileProcessorAdapter {
constructor() {
this.legacyProcessor = new LegacyFileProcessor();
}
processFile(filePath) {
return new Promise((resolve, reject) => {
this.legacyProcessor.processFile(filePath, (err, data) => {
if (err) return reject(err);
resolve(data);
});
});
}
}
module.exports = FileProcessorAdapter;
This approach mirrors the behavior provided by Node’s util.promisify but keeps adaptation logic explicit and testable3.
Using the adapter in application code
const FileProcessorAdapter = require('./FileProcessorAdapter');
const fileProcessor = new FileProcessorAdapter();
async function handleFileProcessing() {
try {
console.log('Processing file with modern async/await...');
const content = await fileProcessor.processFile('my-file.txt');
console.log('Processed Content:', content);
} catch (error) {
console.error('An error occurred:', error);
}
}
handleFileProcessing();
This keeps the legacy code untouched while giving the rest of your codebase a modern interface.
When to Use an Adapter
Use an adapter when two components can’t communicate directly because their interfaces differ. Typical scenarios:
- Integrating a third-party API whose inputs or outputs don’t match your models.
- Wrapping legacy callback APIs so they work with async/await.
- Supporting multiple data sources with different formats by creating one adapter per source.
When not to use an adapter:
- If you control both systems and a small refactor will solve the mismatch, prefer direct refactoring.
- If your goal is to simplify a complex subsystem, consider a Facade instead. A Facade offers a simplified, high-level entry point; an Adapter focuses only on compatibility.
Quick decision checklist
| Situation | Use Adapter? | Why |
|---|---|---|
| Need to use a third-party library with incompatible API | Yes | You can’t change the library, so adapt to it |
| Control both sides and change is small | No | Refactor directly to avoid extra indirection |
| Need a simplified high-level interface to a complex system | No | Facade is a better fit |
| Migrating legacy systems incrementally | Yes | Wrap old components to match new interfaces |
| Multiple differently structured data sources | Yes | Adapters unify them into one shape |
Testing and Performance
Adapters improve testability by decoupling core logic from external systems. You can mock an adapter’s interface to test components in isolation, and you can test adapters separately to verify translation logic.
Performance overhead from an adapter is minimal — typically one extra function call — and is negligible compared with network I/O or database queries. For most web applications, the maintenance and decoupling benefits far outweigh the tiny cost.
Q&A
Q: What problem does the Adapter pattern solve?
A: It resolves interface incompatibilities by translating calls from a client into calls the adaptee understands, letting you reuse existing code without changing it.
Q: How does an Adapter help with legacy code?
A: An Adapter wraps legacy modules and exposes a modern interface, so you can integrate old, stable code into new applications without risky rewrites.
Q: When should I choose an Adapter over other patterns?
A: Choose an Adapter when you need compatibility between two mismatched interfaces. If you want to simplify a whole subsystem, use a Facade instead.
Further reading and internal links
- Deep dive on polymorphism vs inheritance: /blog/polymorphism-vs-inheritance
- Strategies for modernizing legacy systems: /blog/modernizing-legacy-systems
- Adapter pattern reference and examples: GeeksforGeeks2
At Clean Code Guy, we help teams implement practical design patterns that turn brittle, complex codebases into assets that are resilient, testable, and a pleasure to work on. If you’re wrestling with a legacy system or tricky integrations, our Clean Code Audits can give you a clear, actionable roadmap to a healthier architecture. Learn how we can help you ship better code, faster.
AI writes code.You make it last.
In the age of AI acceleration, clean code isn’t just good practice — it’s the difference between systems that scale and codebases that collapse under their own weight.