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
Related rules
- Set timeouts on every call in API design
- Treat data from other APIs as untrusted in Security testing
- Parse input once at the boundary in Architecture
- Fake only what you do not own in Testing
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.