Skip to content

Core API

Eugene Lazutkin edited this page Jul 10, 2026 · 7 revisions

Core API — io

The default export io is the whole library: a callable, the verbs, and the modifier namespaces (io.full, io.stream) and services (io.cache, io.track, …).

import io from 'double-meh';

const data = await io.get('https://api.example.com/x');

Verbs

get, head, post, put, patch, delete (+ del / remove aliases), options — as methods on io and as named ESM exports. Exotic verbs via io.makeVerb('REPORT').

The DELETE aliases. All three names — io.delete, io.del, io.remove — are the same verb. As a method delete is unproblematic, but it cannot be a bare named ESM export (import {delete} is a syntax error), so the named exports are del and remove; both ship, pick whichever reads better and stay consistent:

import {del, remove} from 'double-meh'; // named-import land
await io.delete('/things/7'); // method land — all three are the same function

Signature: verb(url, data?, options?).

  • url — a string, a URL, or an options bag (a reusable endpoint descriptor).
  • data — the frequent optional: the query for reads, the body for writes.
  • options — the rare override bag (as, ifMatch, stream, cache, fetch, …), typed to exclude url.

For reads, null/undefined data → no query. For writes, undefined → no body, but null is a valid JSON-null body. See Requests & options.

Query plus body on a write. The positional data on a write is the body, so the query rides options.query — no tag functions or URL string-building needed (heya/io used a template-tag trick here; double-meh lowers it like every other intent):

await io.post('/units', {name: 'unit-1'}, {query: {dept: deptId}});
// → POST /units?dept=42 with the JSON body {"name":"unit-1"}

Return model — the method declares the shape

Call Returns
io.get(url) (any bare verb) the parsed data
io.full.get(url) the full envelope {data, status, ok, headers, response, …}
io.stream.get(url) a Web ReadableStream (GET-only) — see Streaming
io.stream.put(url, opts) a streaming {writable, readable, response} duplex (also .post, .patch)
io.records.get(url) an async iterable of parsed records (JSONL / json-seq; also .post)
io.sse(url) an async iterable of Server-Sent Events, with reconnect
io.head(url) / io.options(url) the full envelope (metadata verbs — no body, so data is undefined)

The bare verb is the envelope's .data: io.get(url)(await io.full.get(url)).data. Options tune behavior, never the return type.

In TypeScript the verbs are generic: io.get<T>(url)Promise<T>, io.full.get<T>(url)Promise<Envelope<T>>; io.head / io.options return Promise<Envelope>.

The callable io and endpoint descriptors

io(options) is the low-level callable; the verbs are sugar. Because url may be an options bag, a reusable endpoint is natural — the bag is spread (never mutated), and the 3rd arg overrides it per call (scalars replace; headers/query merge per key; url stays from the 1st arg):

const endpoint = {url: '/things/7', headers: {authorization: token}};
await io.get(endpoint); // GET with the auth header
await io.put(endpoint, body, {ifMatch: tag}); // reuse it for a conditional PUT

Services & extensibility

io carries the services and the registration seams:

  • Services: cache, track, retry, mock — each with .attach() / .detach() and per-request options (cache and track are on by default for GETs; pass cache: false / track: false to opt out).
  • Inspectors: io.inspect.request(fn, match?) and io.inspect.response(fn, match?) rewrite requests / envelopes; match scopes the inspector to matching request URLs — a URL prefix string, a RegExp, or a predicate (url) => boolean. The registries (io.requestInspectors, io.responseInspectors) hold {fn, match} entries (see Cookbook: requests & responses).
  • Processors: io.registerData(processor) (encode request bodies by type), io.registerMime(processor) (decode responses by content-type).
  • Transports: io.registerTransport(name, fn), io.defaultTransport — see fetch transport.
  • MIME registry: io.mimeTypes — the mutable alias table behind the as option.
  • Per-API defaults: io.defaults(match?, bag) — scoped option defaults merged below the endpoint descriptor and per-call options; see Requests & options.

Events

io.on(event, fn) / io.off(event, fn) / io.emit(event, ...args) — a tiny synchronous emitter (see events). The library emits: 'request' (request, ctx) when a network run starts, 'success' (envelope, ctx), 'failure' (error, ctx), 'retry' ({attempt, response, error}, ctx) per retry attempt, and 'ready' after the code-forward drain. io.inFlight counts active network runs. Note: a cache hit still runs the pipeline, so it emits request/success.

Instances

The default export is the shared, fully-configured instance. io.create() returns an isolated instance with the same equipment (own registries, services, cache) and none of the parent's configuration — a clean slate. createIO() (a named export) returns a bare pipeline — no transport, no services — for minimal builds that wire their own. Error classes are shared across instances, so one instanceof catch serves them all. When to isolate vs configure the shared instance: Cookbook: multiple APIs.

Low-level helpers

io.run(options) (the raw pipeline, returns the envelope — takes fully-assembled options, so io.defaults bags are not applied), io.toEnvelope(response, options) (wrap a Response you already have), io.makeKey(options) / io.buildUrl(options) (see keys & URLs), io.makeVerb(method).

Error classes ride on io too: io.IOError (the base), io.FailedIO, io.TimedOut, io.BadStatus, plus the isAbort(error) named export — see Response envelope.

See also

Clone this wiki locally