Skip to content

Cookbook: conditional writes

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

Cookbook: conditional writes & idempotency

double-meh lowers REST-correctness intents to the right headers. Background: Requests & options, Response envelope.

Optimistic concurrency (If-Match)

Send the ETag you last saw; the server rejects with 412 if it changed underneath you:

const {data, etag} = await io.full.get('/things/7');
await io.put('/things/7', {...data, name: 'new'}, {ifMatch: etag});

Read-modify-write with automatic conflict-retry

io.update does the whole loop for you — GET + ETag, apply your function, conditional PUT, and on a 412 re-read and re-apply (up to a few times), then invalidate the cache:

await io.update('/things/7', thing => ({...thing, views: thing.views + 1}));

Your function may be async and may return undefined to mean "no change" (a no-op that resolves to the current data). See helpers.

Create-only (If-None-Match: *)

Fail instead of overwriting if the resource already exists:

await io.put('/things/7', thing, {ifNoneMatch: '*'});

Idempotent POST

A bare POST is not auto-retried (it is not safe to replay). Add an idempotency key and the same key is reused across retries, so the server can dedupe:

await io.post('/payments', payment, {idempotencyKey: true, retry: true});
//                                    ^ true → a generated UUID, stable across retries

Pass a string to supply your own key.

Cache revalidation (If-None-Match: <etag>)

A conditional GET returns 304 Not Modified and cache serves the stored body:

await io.get('/things/7', null, {ifNoneMatch: etag});

See also

Clone this wiki locally