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
Cover Image for Adapter Pattern: TypeScript, React & Node.js Examples

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.

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

ConceptDescription
TypeStructural
Primary intentAllow objects with incompatible interfaces to work together
Core ideaWrap the adaptee to expose the target interface
Key problem solvedReuse existing classes without changing their source code
Common use casesThird-party libraries, legacy code, multiple data sources

Structure and Roles

The pattern has four roles:

  1. The Client — the code that needs a specific interface.
  2. The Target Interface — the contract the Client expects.
  3. The Adaptee — the incompatible class or module with needed functionality.
  4. 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

SituationUse Adapter?Why
Need to use a third-party library with incompatible APIYesYou can’t change the library, so adapt to it
Control both sides and change is smallNoRefactor directly to avoid extra indirection
Need a simplified high-level interface to a complex systemNoFacade is a better fit
Migrating legacy systems incrementallyYesWrap old components to match new interfaces
Multiple differently structured data sourcesYesAdapters 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.


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.

1.
Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994). [https://en.wikipedia.org/wiki/Design_Patterns_(book)](https://en.wikipedia.org/wiki/Design_Patterns_(book))
2.
Adapter pattern overview and examples: https://www.geeksforgeeks.org/adapter-pattern/
3.
Node.js documentation for util.promisify, a common approach to convert callbacks to Promises: https://nodejs.org/api/util.html#utilpromisifyoriginal
4.
Stack Overflow Developer Survey 2023, showing the prevalence of JavaScript and web technologies that commonly require integration work: https://survey.stackoverflow.co/2023/
← Back to blog
🙋🏻‍♂️

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.