Guide
Pagination
This page shows how to read a list endpoint that returns one page at a time, whether your API pages by cursor, offset or page number. paginate asks for a page only when you ask for the next one, so you can load page 1, then page 2 when the reader wants more, or walk every page in a loop.
import { createApi, defineRequest, paginate } from 'liaise'
type Item = { id: string; name: string }
type Page = { items: Item[]; cursor?: string }
const listItems = defineRequest<Page, { limit: number; cursor?: string }>()({
method: 'GET',
path: '/items',
})
const api = createApi({ baseUrl: 'https://api.example.com', requests: { listItems } })
const pages = paginate(api.listItems, { limit: 50 }, {
next: (p, prev) => (p.data.cursor ? { ...prev, cursor: p.data.cursor } : undefined),
})
// Nothing has been fetched yet.
async function loadMore() {
const { value: page, done } = await pages.next() // one request per click
if (done) return hideLoadMore()
if (page.error) return showError(page.error)
render(page.data.items)
}
render, showError and hideLoadMore stand for your own code, and your “Load more” button calls loadMore. Creating pages sends nothing, and each click sends one request: /items?limit=50, then ?limit=50&cursor=… with the cursor from the page before, until a page comes back without one and the next click finds done.
Walking every page
For an export or a sync job, where you want every page, loop over the pages with for await:
import { createApi, defineRequest, paginate } from 'liaise'
type Item = { id: string; name: string }
type Page = { items: Item[]; cursor?: string }
const listItems = defineRequest<Page, { limit: number; cursor?: string }>()({
method: 'GET',
path: '/items',
})
const api = createApi({ baseUrl: 'https://api.example.com', requests: { listItems } })
for await (const page of paginate(api.listItems, { limit: 50 }, {
next: (p, prev) => (p.data.cursor ? { ...prev, cursor: p.data.cursor } : undefined),
})) {
if (page.error) break
render(page.data.items)
}
The loop asks for /items?limit=50, then for ?limit=50&cursor=… with each page’s cursor, and stops after the first page that comes back without one. break stops it sooner, and no further page is requested.
Going straight to a page
paginate only moves forward, from each page to the one after it. A numbered pager (“go to page 7”) or a cursor kept in the URL needs one particular page, so there you don’t need paginate. Keep the page number or the cursor in your own state, and call the endpoint with it:
// The cursor lives in the URL, so a reload or a shared link opens the same page.
const cursor = new URLSearchParams(location.search).get('cursor') ?? undefined
const { data, error } = await api.listItems({ limit: 50, cursor })
if (error) showError(error)
else render(data.items)
Your “Next” link then carries data.cursor in its URL. With a numbered pager it’s the same call with the number the reader picked, such as { limit: 50, page: 7 } for an API that pages by number.
Getting to the next page
-
nextreturns the params for the next page. It gets the page just loaded and the params that loaded it, so the usual case is a spread. liaise never has to guess whether your API calls itcursor,page_tokenorafter, and the same shape covers every scheme:// offset next: (p, prev) => p.data.items.length === prev.limit ? { ...prev, offset: prev.offset + prev.limit } : undefined // page number, driven by a Link header next: (p, prev) => p.response.headers.get('link')?.includes('rel="next"') ? { ...prev, page: prev.page + 1 } : undefined -
Return
undefinedornullto stop. Afor awaitloop ends there, and the nextpages.next()givesdonewithout sending a request.
When to stop
- An error page ends it. You get the error page, and then a loop ends, or the next
pages.next()givesdone, because there is no data to read the next cursor from. You see what failed. It never stops quietly. maxPageshas no default. Set it if you want a ceiling, as inpaginate(api.listItems, { limit: 50 }, { next, maxPages: 100 }). liaise doesn’t pick a number, because a silent cut-off at an arbitrary page looks exactly like reaching the last one.- A page you don’t ask for is never requested. Breaking out of a
for awaitloop sends no further request, and neither does apagesyou stop callingnext()on.
Options and pages
- Every other option applies to every page. Any of the
CallOptions, such assignal,timeoutorheaders, goes with each request, so one signal cancels every page, whether you walk them or load them one at a time. paginateyields pages. Read the items from each page yourself. Flattening them would mean guessing which field holds the array.
Next: Polling asks an endpoint again on an interval, to keep data fresh or to wait until something is done.