Skip to content

Cookbook: basics

Eugene Lazutkin edited this page Jul 3, 2026 · 2 revisions

Cookbook: Basics

The everyday moves: pick a verb, send some data, read the result. Every verb has the same shape — verb(url, data?, options?) — and returns a promise of the parsed response body.

Import and make a request

The default export is the io object; the verbs hang off it. io(...) itself is the low-level callable that every verb is sugar over.

import io from 'double-meh';

const user = await io.get('https://api.example.com/users/1');
// user is the parsed JSON body, ready to use

Choose a verb

io.get, io.head, io.post, io.put, io.patch, io.delete (aliases io.del / io.remove), and io.options are all available. The second argument is a query for reads (GET / HEAD / OPTIONS / DELETE) and a body for writes (POST / PUT / PATCH).

await io.get('https://api.example.com/users/1'); // read
await io.post('https://api.example.com/users', {name}); // write
await io.delete('https://api.example.com/users/1'); // read-shaped verb

io.head and io.options resolve to the full envelope (there is no body to unwrap); the rest resolve to the parsed data.

Read with a query

Pass an object as the second argument and it becomes the query string. Arrays repeat the key; null / undefined values are dropped.

await io.get('https://api.example.com/search', {q: 'kettle', page: 2, tags: ['sale', 'new']});
// -> GET /search?q=kettle&page=2&tags=sale&tags=new

The fields, sort, and expand options are comma-joined into the query for you.

await io.get('https://api.example.com/users/1', null, {fields: ['id', 'name'], expand: ['org']});
// -> ?fields=id,name&expand=org

Post JSON

Hand a write verb a plain object and it is JSON.stringify-ed with a Content-Type: application/json header. null is a valid JSON body; undefined sends no body at all.

const created = await io.post('https://api.example.com/things', {name: 'Bob', active: true});

Upload FormData or a Blob

Well-known body types — FormData, Blob, URLSearchParams, ArrayBuffer / typed arrays, and ReadableStream — are passed through untouched, so the platform sets the right Content-Type (including the multipart boundary). Do not set Content-Type yourself for FormData.

const form = new FormData();
form.append('title', 'Sunset');
form.append('photo', fileInput.files[0]);
await io.post('https://api.example.com/photos', form);

const blob = new Blob([bytes], {type: 'application/pdf'});
await io.put('https://api.example.com/docs/1', blob);

Read the full envelope

io.full (and io.full.get, io.full.post, ...) resolves to the response envelope instead of just the body — status, headers, and hoisted metadata like etag, location, and links.

const env = await io.full.post('https://api.example.com/things', {name: 'Bob'});
env.status; // 201
env.data; // parsed body
env.location; // absolute URL, resolved from the Location header
env.etag; // '"v1"', if the server sent one
env.headers; // a plain header dictionary

Handle errors

A non-2xx response throws io.BadStatus, which carries the parsed error body on .data and the code on .status. A network-level failure throws io.FailedIO (its subclass io.TimedOut marks a timeout). Both extend the common base io.IOError, but neither extends the other, so the two instanceof branches are independent. A user abort passes through as the platform AbortError — never wrapped.

try {
  await io.get('https://api.example.com/missing');
} catch (error) {
  if (error instanceof io.BadStatus) {
    console.warn(error.status, error.data); // e.g. 404, {title: 'Nope'}
  } else if (error instanceof io.FailedIO) {
    console.error('network failure', error.message);
  }
}

To keep a non-2xx as data instead of a throw, pass {ignoreBadStatus: true} and read io.full.

const env = await io.full.get('https://api.example.com/maybe', null, {ignoreBadStatus: true});
if (!env.ok) console.warn('soft failure', env.status);

Run requests in parallel

Verbs are just promises — fan them out with Promise.all. Identical concurrent GETs are collapsed into a single network call automatically (see deduping).

const [user, orders, prefs] = await Promise.all([
  io.get('https://api.example.com/users/1'),
  io.get('https://api.example.com/users/1/orders'),
  io.get('https://api.example.com/users/1/prefs')
]);

See also

Clone this wiki locally