How-to library · Developer docs

How-to library

Task-shaped pages: a title that starts with a verb, the situation in a few lines, the steps, the working code in both engines, and why this way rather than another. Found from a search, done, and left.

Page through a large API result set without timing out

A REST list with no limit returns everything in one response, which times out on a large site. Page by id instead: order by id, ask for ids above the last one seen, stop on a short page, and resume from the stored id next time.

Context

Context. A REST list with no limit returns every matching record in one response. For a job that reads all products, orders or form submissions, that response grows until the request times out. Read it in pages instead, and page by id rather than by page number: order by id:ASC, ask for the records with an id above the last one you saw (the >id filter), and stop when a page comes back short. Records created or deleted while the job runs then cannot shift the pages under it, which is what makes page-number paging read a record twice or skip one.

Steps

  1. Order by id:ASC and pick a page size of a few hundred with limit.
  2. Select only the fields the job needs with properties. A response costs records times fields times relations.
  3. Send >id= the highest id you have read, starting from 0. In a URL the > is encoded as %3E, which URLSearchParams does for you.
  4. Stop when a page holds fewer records than limit. Use totalCount from the first page only for progress reporting.
  5. Store the highest id at the end, so the next run reads only what is new.
  6. Retry a failed page a few times with a pause; give up on a 4xx, which will not fix itself.

Code

JavaScript
// sync.js: runs on a server (Node 18 or later)
const SITE = process.env.COREDNA_URL;      // https://www.example.com
const KEY  = process.env.COREDNA_API_KEY;
const LIMIT = 250;

async function getJson(url) {
  for (let attempt = 1; ; attempt++) {
    const response = await fetch(url, { headers: { Authorization: 'Bearer ' + KEY } });
    if (response.ok) return response.json();
    if (attempt === 3 || response.status < 500) {
      throw new Error(url + ': HTTP ' + response.status + ' ' + (response.headers.get('x-reason') || ''));
    }
    await new Promise(r => setTimeout(r, 2000 * attempt));
  }
}

// Every product variant with an id above sinceId, LIMIT at a time
export async function* variantsSince(sinceId = 0) {
  let lastId = sinceId;
  for (;;) {
    const url = new URL(SITE + '/cdna-api/products/product-variants');
    url.searchParams.set('properties', '{ id, code, name, finalPrice, availableQuantity }');
    url.searchParams.set('orderBy', 'id:ASC');
    url.searchParams.set('limit', LIMIT);
    url.searchParams.set('>id', lastId);

    const page = await getJson(url);
    for (const variant of page.data) {
      yield variant;
      lastId = variant.id;
    }
    if (page.data.length < LIMIT) return;
  }
}

// usage: resume from the id stored by the previous run
let highest = Number(process.env.LAST_SYNCED_ID || 0);
for await (const variant of variantsSince(highest)) {
  await upsertIntoWarehouse(variant);   // your code
  highest = variant.id;
}
console.log('synced up to id', highest);

The request each loop sends, readable:

HTTP
GET /cdna-api/products/product-variants?properties={ id, code, name, finalPrice, availableQuantity }&orderBy=id:ASC&limit=250&%3Eid=4180
Authorization: Bearer YOUR_ACCESS_TOKEN

Why this and not that

Not one request with no limit: it works until the data outgrows the timeout, which happens in production and not on the staging site. Not page=1, 2, 3…: a record deleted from an early page moves every later record back by one, so one is skipped; a record created with a low position pushes one forward, so one is read twice. It is the right tool for a listing a person clicks through, not for a job. Not ordering by modified or by name: records edited during the run move. Not GraphQL: it has first and offset but no totalCount, and a job gains nothing from its shaping. And take the field names from your site's /docs/module/Products/model/ProductVariant/Api page: a misspelt one does not fail the request, it comes back as an error string inside every record at HTTP 200.

Go deeper

Was this page helpful?

Your answer goes to the docs team.