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
Cover Image for 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.

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

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 main 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 that has the 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, 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

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 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.


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.