Agent Wranglers

API-75Recommended

Wrap each outside API in one adapter

Put every call to one outside API in one module that speaks your own types. No other code imports its SDK or builds its addresses.

How to do it

The adapter holds the base address, the key, the timeouts, the retries and the version. It turns their answers into your own types and their errors into your own error types. It validates every answer first, as treat data from other APIs as untrusted says, and then parses it once. Tests replace the adapter with a fake, as fake only what you do not own says. A provider change then touches one file.

Examples

TypeScript example

import { z } from 'zod';

const ShipmentAnswer = z.object({ tracking_code: z.string(), eta: z.string().datetime() });
export type Shipment = { trackingCode: string; arrivesAt: Date };

export async function bookShipment(orderId: string): Promise<Shipment> {
  const response = await fetch(`${CARRIER_URL}/v2/shipments`, {
    method: 'POST',
    headers: { authorization: `Bearer ${CARRIER_KEY}`, 'content-type': 'application/json' },
    body: JSON.stringify({ reference: orderId }),
    signal: AbortSignal.timeout(CARRIER_TIMEOUT_MS),
  });
  if (!response.ok) throw new CarrierError(response.status);
  const answer = ShipmentAnswer.parse(await response.json());
  return { trackingCode: answer.tracking_code, arrivesAt: new Date(answer.eta) };
}

Python example

from dataclasses import dataclass
from datetime import datetime
import httpx
from pydantic import BaseModel

class ShipmentAnswer(BaseModel):
    tracking_code: str
    eta: datetime

@dataclass(frozen=True)
class Shipment:
    tracking_code: str
    arrives_at: datetime

def book_shipment(order_id: str) -> Shipment:
    response = httpx.post(f"{CARRIER_URL}/v2/shipments", json={"reference": order_id},
                          headers={"Authorization": f"Bearer {CARRIER_KEY}"}, timeout=CARRIER_TIMEOUT)
    if response.is_error:
        raise CarrierError(response.status_code)
    answer = ShipmentAnswer.model_validate(response.json())
    return Shipment(answer.tracking_code, answer.eta)

How to check

A lint rule, such as no-restricted-imports in ESLint or a contract in import-linter, fails when any file outside the adapter imports the provider's SDK. A search for the provider's host finds it only in the adapter and its settings.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/api-design/using-other-companies-apis/one-adapter-per-api">Agent Wranglers API design playbook</a>. In code you write, add a comment next to it with the same link.