-
Notifications
You must be signed in to change notification settings - Fork 0
Cookbook: multiple apis
Real apps rarely talk to one service. There are two ways to give each API its own settings, and
they compose: scoped configuration on the shared instance for different settings, and
isolated instances (io.create()) for different machinery. Start with the first; reach for
the second when state must not be shared.
Cache and dedup keys are URL-scoped, so two APIs cannot collide in the shared cache — different settings do not require different instances. Configure each API once at setup; call sites stay one-liners.
import io from 'double-meh';
// per-API option defaults — the lowest merge layer (per-call options always win)
io.defaults('https://legacy.example.com/', {
listSeparator: ',', // this API wants ?tags=a,b
timeout: 10_000, // and it is slow
accept: 'application/json'
});
io.defaults('https://modern.example.com/', {
accept: 'application/vnd.modern+json'
});
// per-API auth — inspectors scoped by URL prefix (RegExp and predicates work too)
io.inspect.request(request => {
request.headers.set('Authorization', 'Bearer ' + legacyToken());
}, 'https://legacy.example.com/');
io.inspect.request(request => {
request.headers.set('X-Api-Key', modernKey());
}, 'https://modern.example.com/');
// per-API service defaults — theDefault predicates
io.cache.theDefault = options => !String(options.url).startsWith('https://volatile.example.com/');
// per-API mocks in tests — the matcher scopes them (trailing * = prefix; bare string = exact)
io.mock('https://legacy.example.com/*', () => new Response('{"ok":true}'));// call sites don't know or care which API has which conventions
const units = await io.get('https://legacy.example.com/units', {tags: ['a', 'b']}); // ?tags=a,b
const items = await io.get('https://modern.example.com/items'); // vendor accept, no separatorEndpoint descriptors are the per-endpoint rung of the same ladder — a reusable options bag pins the URL and any per-endpoint settings, and every verb shares it.
io.create() returns a fresh, fully-equipped instance: its own transports, inspectors, defaults,
data/MIME processors, mimeTypes aliases, encoders, events, and services with their own state —
own cache storage, own mock registry, own bundler config. It is a clean slate: nothing of the
parent's configuration is inherited.
Use it when an API needs different machinery, not just different settings:
import io from 'double-meh';
import {sqliteStorage} from 'double-meh/storage/sqlite.js';
import {installAcme} from './acme-plugin.js';
const legacy = io.create();
installAcme(legacy); // an envelope plugin that unwraps this API's {v, payload} envelopes
legacy.defaults({listSeparator: null, timeout: 10_000}); // unscoped: the whole instance is one API
legacy.cache.storage = await sqliteStorage(); // its own persistent cache
const modern = io.create(); // sees none of the above
modern.bundle.url = 'https://modern.example.com/bundle'; // its own bundler windowThe clearest reason to isolate: a pluggable envelope that
matches a shared MIME type (plain application/json) would hijack decoding for every service on
the instance — containment is isolation. The same goes for a different cache backend, a different
default transport, or SDK-grade separation where independent consumers must not see each other's
inspectors, mocks, or events.
Every instance carries the same error classes — io.IOError, io.FailedIO, io.BadStatus,
io.TimedOut — so one catch block serves requests from any instance:
try {
await Promise.all([legacy.get(legacyUrl), modern.get(modernUrl)]);
} catch (error) {
if (error instanceof io.BadStatus) {
// works no matter which instance threw; error.problem is the parsed envelope either way
console.error(error.status, error.problem);
}
}What differs per instance is error behavior, not identity: each instance's MIME processors decide
how its error bodies parse into BadStatus.problem, so an envelope plugin's fault
handling stays contained to its instance.
| Need | Reach for |
|---|---|
per-API timeouts, accept, listSeparator, retry policy |
io.defaults(match, bag) |
| per-API auth headers, URL rewriting | scoped inspectors |
| per-host cache/track opt-outs |
theDefault predicates |
| per-endpoint settings shared across verbs | an endpoint descriptor |
| an envelope plugin on a shared MIME type | io.create() |
| a different cache backend / transport default / bundler | io.create() |
| independent consumers (SDKs) that must not see each other |
io.create() per consumer |
- Core API — instances and endpoint descriptors
-
Requests & options —
io.defaultsandlistSeparator - Cookbook: requests & responses — inspector recipes
- Concepts: pluggable envelopes — when plugins force isolation
Guides
Concepts
Services
Modules
- fetch transport
- keys & URLs
- helpers
- records
- sse
- storage backends
- compression encoders
- Service Worker integration
Cookbook