Release candidate — 1.0.0-rc.7
StitchAPI
GuidesTransport & adapters

Writing an adapter

Implement the one-function Adapter contract to carry a stitch over a transport StitchAPI doesn't ship, and declare what it supports.

Write your own Adapter when the bytes need to move a way the built-ins don't cover — over a platform HTTP client, through an instrumentation layer, or across a transport that isn't HTTP at all. The contract is one function, so most custom adapters are a thin wrapper rather than a rewrite.

Example

A timing wrapper around another adapter — it delegates the round trip and adds nothing to the request:

import { type , ,  } from 'stitchapi';

const  =
    (: ):  =>
    async () => {
        const  = .();
        const  = await ();
        .(
            `${.} ${.} → ${.}`,
            .() - ,
        );
        return ;
    };

const  = ({
    : 'https://api.example.com',
    : '/users/{id}',
    : (()),
});

Options

An adapter is (req: AdapterRequest) => Promise<AdapterResponse> — a plain function, so nothing needs to be registered or subclassed. Four rules make it a transport the engine can drive:

  • Never throw on a non-2xx status. A 500 resolves as { status: 500, headers, body }. Only a genuine network failure or an abort rejects. The stitch owns what a status means — whether 503 retries, whether 404 is an error or an empty result — so a throwing transport takes that decision away from the declaration.
  • Lowercase the response header names. Readers above you look them up in that case.
  • Honor req.signal. Abort the in-flight request when it fires, and reject if it is already aborted.
  • Reject req.stream if you can't stream. Failing loudly at the call beats buffering a stream the caller expected to consume incrementally.

req carries what the transport needs and nothing about policy: { url, method, headers, body?, bodyType?, multipart?, responseType?, stream?, onProgress?, signal? }. The response is { status, headers, body, url? }, where body is the parsed value — or a ReadableStream<Uint8Array> when stream was set. See Reference → Config types for the full shapes.

Declare what it supports

Hang a capabilities descriptor off the function and the engine can answer for your transport instead of going quiet — a call asking for upload progress on an adapter that can't report it gets an info event rather than a bar that never moves.

const :  = ;

. = {
    : 'myAdapter',
    : ['downloadProgress'], // anything not listed, it can't do
};

Declaring is opt-in. An adapter that declares nothing is treated as unknown and no checks run against it, so the plain one-function form stays valid.

Anti-pattern: don't reach for a custom adapter to add retries, timeouts, auth headers, or logging of the event stream — a stitch already declares those as fields, and burying them in a transport hides them from the declaration, the trace, and every other surface. Write one when the transport differs; use retry, timeout, auth, and trace sinks for the rest.

Prove it against the contract

verifyAdapterContract(adapter, baseUrl) from stitchapi/testing runs your transport against a fixture host and checks the rules above: status passthrough for 200/404/500, request delivery, lowercased response headers, text and JSON decoding, and both abort paths. Mount adapterContractFixture on any server — node:http, hono, a service worker — and point the verifier at it. See Conformance kits for the fixture and the report shape.

See also

On this page