Data fetching and pagination

Use list responses, cursors, async pagination, and framework cache directives correctly.

SDK list methods return an envelope, not a bare array. Read one bounded page for interactive screens and use paginate only when a background task must walk multiple pages.

Read a page

ts
const page = await board.jobs.list({
limit: 20,
remoteOption: ['remote'],
sort: 'newest',
});
console.log(page.data);
console.log(page.hasMore);
console.log(page.nextCursor);

Every list envelope contains:

  • data — the typed items on this page.
  • hasMore — whether another page exists.
  • nextCursor — the opaque cursor for that page, or null at the end.
  • object and url — the envelope type and API path.

Job browse and search envelopes can also include catalog fields such as count, limit, offset, and gatedCount.

The query limit is the requested page size. On jobs it accepts 1–100. It is unrelated to the maximum number of items you might collect across pages.

Build numbered pages with offset

Most user-facing directories work best with a stable ?page=2 URL. Convert the page number to an offset:

ts
const pageNumber = Math.max(1, Number(searchParams.page ?? '1'));
const limit = 20;
const page = await board.jobs.list({
limit,
offset: (pageNumber - 1) * limit,
sort: 'newest',
});

Keep the page number, filters, and search terms in the URL. Offset is for direct access to a numbered page; it is not a cursor and should not be carried into a catalog walk.

Request the next cursor page

Echo nextCursor exactly as returned. Do not inspect it, decode it, or construct one yourself.

ts
const first = await board.jobs.list({ limit: 20 });
const second = first.nextCursor
? await board.jobs.list({ limit: 20, cursor: first.nextCursor })
: null;

Stop when hasMore is false or nextCursor is null. If the request includes offset, offset takes precedence over the cursor for catalog reads. Use offset for numbered pages; do not carry it into a cursor walk.

Walk a complete catalog

paginate carries each cursor forward and yields individual items:

ts
import { paginate } from '@cavuno/board';
for await (const job of paginate(board.jobs.list, {
limit: 100,
sort: 'newest',
})) {
console.log(job.title);
}

Use pages() when you need each raw envelope:

ts
const pager = paginate(board.jobs.list, { limit: 100 });
for await (const page of pager.pages()) {
console.log(page.data.length, page.nextCursor);
}

For methods with a required leading argument, close over that argument:

ts
const companyJobs = paginate(
(query, options) => board.companies.listJobs('acme', query, options),
{ limit: 100 },
);

Collect with a hard cap

toArray requires a cap so a large board cannot exhaust memory:

ts
const first500 = await paginate(board.jobs.list, { limit: 100 }).toArray({
limit: 500,
});

The two limits have different jobs:

  • The query limit: 100 requests up to 100 items per API call.
  • toArray({ limit: 500 }) stops after collecting 500 items.

Always set both deliberately for feeds, sitemaps, and exports.

Separate query data from fetch options

Filters and pagination belong in the method’s query argument. Transport behavior belongs in the final FetchOptions argument:

ts
const controller = new AbortController();
const page = await board.jobs.list(
{ limit: 20, employmentType: ['full_time'] },
{ signal: controller.signal },
);

Everything except the SDK’s body and query fields passes through to fetch. That includes standard fields such as signal, headers, and cache, plus runtime extensions such as Next.js next and Cloudflare cf.

ts
await board.jobs.list(
{ limit: 20 },
{ next: { revalidate: 60, tags: ['jobs'] } },
);
await board.jobs.list(
{ limit: 20 },
{ cf: { cacheTtl: 60 } } as never,
);

The type cast in the Workers example is needed when the project uses standard DOM RequestInit types that do not declare Cloudflare’s cf extension.

Choose the correct fetch pattern

  • Use a single bounded request for search results and user-facing lists.
  • Use paginate for finite background work such as a sitemap or feed.
  • Add an AbortSignal to search-as-you-type and navigation-bound requests.
  • Specify an explicit sort when stable traversal order matters.
  • Never place candidate-specific or employer-specific responses in a shared public cache.

Verify pagination

Test both navigation models with more records than one page. For numbered pages, verify page 1 uses offset 0, page 2 uses the page size, and filters survive the URL transition. For cursor navigation, record each returned cursor and confirm the next request echoes it unchanged until nextCursor is null. Also test an initial offset: paginate must drop it after the first request so it cannot override later cursors.

If a cursor is invalid or stale, the API throws a typed error such as pagination_invalid_cursor. Restart the walk from a fresh first page; do not modify the cursor.

Next, learn how to handle SDK errors.