Claude Code Plugins

Community-maintained marketplace

Feedback
0
0

Wrap third-party libraries for testability and replaceability. Use when integrating external APIs, creating testable code, or building swappable implementations.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name dependency-wrapper
description Wrap third-party libraries for testability and replaceability. Use when integrating external APIs, creating testable code, or building swappable implementations.
allowed-tools Read, Write, Edit, Glob, Grep

Dependency Wrapper Skill

Wrap third-party libraries for testability, replaceability, and controlled dependency injection.


When to Use

  • Integrating third-party APIs or libraries
  • Creating testable code with external dependencies
  • Building swappable implementations
  • Managing configuration for external services
  • Isolating vendor-specific code

Philosophy

Never call third-party libraries directly in business logic.

Instead:

  1. Create a wrapper module that exposes only what you need
  2. Inject the wrapper where needed
  3. Mock the wrapper in tests
  4. Swap implementations without changing business code

Wrapper Patterns

Simple Wrapper

Wrap a library to expose a simplified interface:

// lib/date.js
// Wrapper around date-fns (or any date library)

import { format, parseISO, addDays, differenceInDays } from 'date-fns';

/**
 * Date utilities wrapper
 * Swap date-fns for another library by changing only this file
 */
export const dateUtils = {
  /**
   * Format date for display
   * @param {Date|string} date
   * @param {string} formatStr - e.g., 'yyyy-MM-dd'
   * @returns {string}
   */
  format(date, formatStr = 'yyyy-MM-dd') {
    const d = typeof date === 'string' ? parseISO(date) : date;
    return format(d, formatStr);
  },

  /**
   * Add days to a date
   * @param {Date} date
   * @param {number} days
   * @returns {Date}
   */
  addDays(date, days) {
    return addDays(date, days);
  },

  /**
   * Get days between two dates
   * @param {Date} dateA
   * @param {Date} dateB
   * @returns {number}
   */
  daysBetween(dateA, dateB) {
    return differenceInDays(dateA, dateB);
  },

  /**
   * Parse ISO string to Date
   * @param {string} isoString
   * @returns {Date}
   */
  parse(isoString) {
    return parseISO(isoString);
  }
};

Factory Pattern

Create instances with configuration:

// lib/http-client.js
// Wrapper around fetch or axios

/**
 * Create configured HTTP client
 * @param {object} config
 * @returns {object} HTTP client
 */
export function createHttpClient(config = {}) {
  const {
    baseUrl = '',
    timeout = 10000,
    headers = {}
  } = config;

  async function request(path, options = {}) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    try {
      const response = await fetch(`${baseUrl}${path}`, {
        ...options,
        headers: { ...headers, ...options.headers },
        signal: controller.signal
      });

      clearTimeout(timeoutId);

      if (!response.ok) {
        throw new HttpError(response.status, await response.text());
      }

      return response.json();
    } catch (error) {
      clearTimeout(timeoutId);
      throw error;
    }
  }

  return {
    get: (path, options) => request(path, { ...options, method: 'GET' }),
    post: (path, body, options) => request(path, {
      ...options,
      method: 'POST',
      body: JSON.stringify(body),
      headers: { 'Content-Type': 'application/json', ...options?.headers }
    }),
    put: (path, body, options) => request(path, {
      ...options,
      method: 'PUT',
      body: JSON.stringify(body),
      headers: { 'Content-Type': 'application/json', ...options?.headers }
    }),
    delete: (path, options) => request(path, { ...options, method: 'DELETE' })
  };
}

class HttpError extends Error {
  constructor(status, body) {
    super(`HTTP ${status}`);
    this.status = status;
    this.body = body;
  }
}

Adapter Pattern

Normalize different implementations behind a common interface:

// lib/storage-adapter.js
// Adapter for different storage backends

/**
 * @typedef {object} StorageAdapter
 * @property {(key: string) => Promise<*>} get
 * @property {(key: string, value: *) => Promise<void>} set
 * @property {(key: string) => Promise<void>} remove
 */

/**
 * LocalStorage adapter
 * @returns {StorageAdapter}
 */
export function createLocalStorageAdapter() {
  return {
    async get(key) {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : null;
    },
    async set(key, value) {
      localStorage.setItem(key, JSON.stringify(value));
    },
    async remove(key) {
      localStorage.removeItem(key);
    }
  };
}

/**
 * IndexedDB adapter
 * @param {string} dbName
 * @returns {StorageAdapter}
 */
export function createIndexedDBAdapter(dbName) {
  // ... IndexedDB implementation
  return {
    async get(key) { /* ... */ },
    async set(key, value) { /* ... */ },
    async remove(key) { /* ... */ }
  };
}

/**
 * In-memory adapter (for testing)
 * @returns {StorageAdapter}
 */
export function createMemoryAdapter() {
  const store = new Map();
  return {
    async get(key) {
      return store.get(key) ?? null;
    },
    async set(key, value) {
      store.set(key, value);
    },
    async remove(key) {
      store.delete(key);
    }
  };
}

Dependency Injection

Service Container

Simple dependency container:

// lib/container.js

/**
 * Simple dependency injection container
 */
class Container {
  constructor() {
    this.services = new Map();
    this.singletons = new Map();
  }

  /**
   * Register a service factory
   * @param {string} name
   * @param {Function} factory
   * @param {object} options
   */
  register(name, factory, { singleton = false } = {}) {
    this.services.set(name, { factory, singleton });
  }

  /**
   * Get a service instance
   * @param {string} name
   * @returns {*}
   */
  get(name) {
    const service = this.services.get(name);
    if (!service) {
      throw new Error(`Service not registered: ${name}`);
    }

    if (service.singleton) {
      if (!this.singletons.has(name)) {
        this.singletons.set(name, service.factory(this));
      }
      return this.singletons.get(name);
    }

    return service.factory(this);
  }
}

export const container = new Container();

// Register services
container.register('config', () => ({
  apiUrl: process.env.API_URL,
  // ...
}), { singleton: true });

container.register('httpClient', (c) => {
  const config = c.get('config');
  return createHttpClient({ baseUrl: config.apiUrl });
}, { singleton: true });

container.register('userService', (c) => {
  return createUserService(c.get('httpClient'));
});

Constructor Injection

Pass dependencies to constructors:

// services/user-service.js

/**
 * User service with injected HTTP client
 * @param {object} httpClient - HTTP client instance
 */
export function createUserService(httpClient) {
  return {
    async getUser(id) {
      return httpClient.get(`/users/${id}`);
    },

    async createUser(data) {
      return httpClient.post('/users', data);
    },

    async updateUser(id, data) {
      return httpClient.put(`/users/${id}`, data);
    }
  };
}

Testing with Wrappers

Mock Factory

Create test doubles easily:

// test/mocks/http-client.js

/**
 * Create mock HTTP client for testing
 * @param {object} responses - Map of path to response
 */
export function createMockHttpClient(responses = {}) {
  const calls = [];

  return {
    async get(path) {
      calls.push({ method: 'GET', path });
      const response = responses[`GET ${path}`] ?? responses[path];
      if (response instanceof Error) throw response;
      return response;
    },

    async post(path, body) {
      calls.push({ method: 'POST', path, body });
      const response = responses[`POST ${path}`] ?? responses[path];
      if (response instanceof Error) throw response;
      return response;
    },

    // ... other methods

    // Test helpers
    getCalls() {
      return calls;
    },

    wasCalledWith(method, path) {
      return calls.some(c => c.method === method && c.path === path);
    }
  };
}

Testing Example

// test/services/user-service.test.js
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { createUserService } from '../../src/services/user-service.js';
import { createMockHttpClient } from '../mocks/http-client.js';

describe('UserService', () => {
  it('fetches user by id', async () => {
    const mockHttp = createMockHttpClient({
      'GET /users/123': { id: '123', name: 'John' }
    });

    const userService = createUserService(mockHttp);
    const user = await userService.getUser('123');

    assert.equal(user.name, 'John');
    assert.ok(mockHttp.wasCalledWith('GET', '/users/123'));
  });

  it('handles errors', async () => {
    const mockHttp = createMockHttpClient({
      'GET /users/999': new Error('Not found')
    });

    const userService = createUserService(mockHttp);

    await assert.rejects(
      () => userService.getUser('999'),
      /Not found/
    );
  });
});

Common Wrappers

Email Service

// lib/email.js
// Swap SendGrid, Mailgun, SES by changing this file

/**
 * Create email service
 * @param {object} config
 */
export function createEmailService(config) {
  const { provider = 'console', apiKey } = config;

  const providers = {
    console: {
      async send({ to, subject, html }) {
        console.log(`Email to ${to}: ${subject}`);
        return { id: 'console-' + Date.now() };
      }
    },
    sendgrid: {
      async send({ to, subject, html }) {
        // SendGrid implementation
      }
    }
  };

  return providers[provider] || providers.console;
}

Payment Service

// lib/payments.js

/**
 * Payment gateway adapter
 */
export function createPaymentService(config) {
  const { provider, apiKey } = config;

  return {
    async createPayment(amount, currency, metadata) {
      // Route to configured provider
    },

    async refund(paymentId, amount) {
      // ...
    },

    async getPayment(paymentId) {
      // ...
    }
  };
}

Checklist

When wrapping dependencies:

  • Create wrapper module in lib/ or adapters/
  • Expose only the interface you need
  • Use factory functions for configuration
  • Accept dependencies as constructor parameters
  • Create mock factories for testing
  • Document the wrapper's public API with JSDoc
  • Keep vendor-specific code isolated in wrapper
  • Use adapters for swappable implementations
  • Register dependencies in container if using DI
  • Write tests using mock implementations

Related Skills

  • unit-testing - Write unit tests for JavaScript files using Node.js nativ...
  • api-client - Fetch API patterns with error handling, retry logic, and ...
  • authentication - Implement secure authentication with JWT, sessions, OAuth...
  • nodejs-backend - Build Node.js backend services with Express/Fastify, Post...