Ever had a library or legacy module that won’t fit with the rest of your system? The Adapter pattern translates one interface into another so you can reuse existing code without changing it. This guide provides practical TypeScript, React, and Node.js examples you can apply immediately to simplify integrations and improve testability.
December 19, 2025 (2mo ago) — last updated February 10, 2026 (29d ago)
Adapter Pattern: TypeScript, React & Node.js Examples
Learn how the Adapter pattern connects incompatible interfaces with TypeScript, React, and Node.js examples to modernize integrations and legacy code.
← Back to blog
Adapter Pattern: TypeScript, React & Node.js Examples
Summary: Learn how the Adapter pattern connects incompatible interfaces in TypeScript, React, and Node.js with practical, real-world examples.
Introduction
Ever had a perfectly good library or legacy module that simply won’t fit with the rest of your system? It’s like trying to plug a European adapter into a North American socket — both work, but their interfaces don’t match. The Adapter pattern solves that by translating one interface into another so you can reuse existing code without changing it.
This guide explains the pattern, shows TypeScript and React examples, and demonstrates a Node.js adapter that modernizes callback-based modules. It focuses on practical techniques you can apply immediately to clean up integrations and improve testability.
Why the Adapter Pattern Matters
The Adapter pattern is a structural pattern that wraps an incompatible object and exposes the interface your code expects. It was first documented by the Gang of Four in 19941. Adapters are essential for integrating third-party APIs, modernizing legacy code, and unifying disparate data sources.
Common scenarios where adapters help:
- Integrating third-party APIs that return different data shapes.
- Modernizing legacy callback APIs to work 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 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 with 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, components become 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,
};
}
Centralizing transformations keeps components 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, which makes testing and reuse straightforward.
Example: Modernizing a Callback-Based Node.js Module
Legacy modules often use error-first callbacks. Instead of modifying a stable 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 mirrors Node’s util.promisify behavior 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 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. JavaScript remains the most-used language in the Stack Overflow Developer Survey, highlighting how often developers face integration work that adapters solve4.
Frequently asked questions
Q: What problem does the Adapter pattern solve?
A: It resolves interface incompatibilities by translating calls from a client into calls the adaptee understands so you can reuse code without changing it.
Q: How does an Adapter help with legacy code?
A: An Adapter wraps legacy modules and exposes a modern interface, letting you 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.