-
Notifications
You must be signed in to change notification settings - Fork 0
Cookbook: adapting endpoints
Components that fetch their own data — a table, a form, a chart — are only reusable if they can assume one response shape. Real endpoints rarely agree on one. Rather than teaching every component about every vendor, adapt at the I/O layer: a pair of URL-scoped hooks turns a vendor's shape into your house shape, once, for every consumer that touches that endpoint.
The house shape used throughout this wiki is the one
io.paginate understands natively — data, offset, limit, an optional total, and a
links object with prev/next.
A response inspector receives the finished envelope and may rewrite it in place. This is
the whole adapter for a Django REST Framework collection ({count, next, previous, results}):
import io from 'double-meh';
io.inspect.response(envelope => {
const body = envelope.data;
if (!body || !Array.isArray(body.results)) return; // not a collection — leave it alone
envelope.data = {
data: body.results,
total: body.count,
links: {next: body.next, prev: body.previous}
};
}, 'https://api.example.com/');Every consumer of that host now sees the house envelope — including io.paginate, which will follow
links.next without knowing DRF exists:
for await (const row of io.paginate('https://api.example.com/products')) render(row);Guard the shape before rewriting, as above. An inspector scoped to a host still sees every response
from it, collections and single objects alike, so a bare envelope.data = {...} would mangle the
ones you did not mean.
A request inspector runs on the prepared request and can rewrite the URL — useful when a vendor spells the paging parameters differently:
io.inspect.request(request => {
const url = new URL(request.url); // absolute: the match below is an absolute prefix
const q = url.searchParams;
for (const [ours, theirs] of [
['offset', 'skip'],
['limit', 'take']
]) {
if (!q.has(ours)) continue;
q.set(theirs, q.get(ours));
q.delete(ours);
}
request.url = url.href;
}, 'https://vendor.example.com/');For anything that is a plain default rather than a transformation, prefer io.defaults — it is the
lowest merge layer, so per-call options still win:
io.defaults('https://vendor.example.com/', {
accept: 'application/vnd.vendor+json',
listSeparator: ',' // this vendor wants ?tags=a,b rather than repeated keys
});Every hook takes an optional match — a URL prefix, a RegExp, or a predicate:
io.inspect.response(normalizeDrf, 'https://api.example.com/'); // prefix
io.inspect.response(normalizeOData, /\/odata\//); // RegExp
io.inspect.response(normalizeLegacy, url => url.includes('/v1/')); // predicateUnscoped hooks run for every request, so give an adapter the narrowest match that covers its endpoint.
When two APIs need genuinely different machinery rather than different settings — separate caches,
separate services — reach for io.create() instead; see
Cookbook: multiple APIs.
When a vendor ships its own media type rather than a differently-shaped JSON, the seam is
registerMime (decode) and registerData (encode) rather than an inspector — the decode runs on
error bodies too, so BadStatus.problem picks the vendor's fault envelope up for free. Response
inspectors are for reworking shared MIME types that must not be hijacked globally. See
Concepts: pluggable envelopes.
The two sides are not symmetric, and the difference shows up in the cache:
| Hook | Runs | Affects the cache key? | Cache stores |
|---|---|---|---|
io.defaults |
lowest merge layer, before the request is built | yes | — |
io.inspect.request |
after prepare, before request identity |
yes | — |
io.inspect.response |
after the service onion, on the way out | no | the wire body |
So a request-side rewrite canonicalizes identity too — two vendor spellings of one resource collapse onto a single cache and dedup entry.
A response-side rewrite does not, and that is deliberate: the cache is a wire cache. It holds what
the server actually sent, which is what keeps ETag/304 revalidation and Vary selection meaningful —
a reshaped entry would no longer correspond to any HTTP representation the server could revalidate.
It also keeps your adapter swappable: edit a transform and every cached entry stays valid, whereas a
cache of transformed bodies would keep serving the old shape until eviction. Transforms sit outside
the cache, and re-running one on each delivery costs a field reshuffle — nothing next to the request
it saved. Only worry about it if a response inspector does something genuinely expensive.
- Cookbook: multiple APIs — scoped configuration and isolated instances
- Concepts: pluggable envelopes — the encode/decode seams
-
helpers —
io.paginateand the envelope it walks - Requests & options — the option surface an adapter shapes
Guides
Concepts
Services
Modules
- fetch transport
- keys & URLs
- helpers
- records
- sse
- storage backends
- compression encoders
- Service Worker integration
Cookbook