Pagination
Description
Section titled “Description”This example demonstrates how to properly handle pagination across multiple indexer endpoints using limit and next parameters. It includes a generic pagination helper function and shows iteration through all pages of results.
Prerequisites
Section titled “Prerequisites”- LocalNet running (via
algokit localnet start)
Run This Example
Section titled “Run This Example”From the repository root:
cd examplesnpm run example indexer_client/16-pagination.ts/** * Example: Pagination * * This example demonstrates how to properly handle pagination across multiple * indexer endpoints using limit and next parameters. It includes a generic * pagination helper function and shows iteration through all pages of results. * * Prerequisites: * - LocalNet running (via `algokit localnet start`) */
import { algo } from '@algorandfoundation/algokit-utils'import { createAlgorandClient, createIndexerClient, formatMicroAlgo, printError, printHeader, printInfo, printStep, printSuccess, shortenAddress,} from '../shared/utils.js'
// ============================================================================// Generic Pagination Helper Function// ============================================================================
/** * Generic pagination options for fetch function */interface PaginationOptions { /** Maximum items per page */ limit?: number /** Token for next page */ next?: string}
/** * Generic response type for paginated endpoints */interface PaginatedResponse<T> { /** The items returned in this page */ items: T[] /** Token for fetching the next page (undefined if no more pages) */ nextToken?: string /** Current round when query was performed */ currentRound: bigint}
/** * Options for the paginateAll helper */interface PaginateAllOptions<T> { /** Page size (default: 100) */ pageSize?: number /** Maximum total items to fetch (default: unlimited) */ maxItems?: number /** Callback called for each page, return false to stop pagination */ onPage?: (items: T[], pageNumber: number) => boolean | void /** Condition to stop early - return true to stop */ stopWhen?: (item: T, index: number) => boolean}
/** * Generic pagination helper that iterates through all pages of results. * * This function provides a reusable pattern for paginating through any * indexer endpoint that supports limit and next parameters. * * @param fetchPage - Function that fetches a page of results * @param options - Pagination options including pageSize, maxItems, callbacks * @returns All items collected across all pages * * @example * // Fetch all transactions from an account * const allTxns = await paginateAll( * async (opts) => { * const result = await indexer.searchForTransactions({ ...opts }) * return { * items: result.transactions, * nextToken: result.nextToken, * currentRound: result.currentRound, * } * }, * { pageSize: 100, maxItems: 1000 } * ) */async function paginateAll<T>( fetchPage: (options: PaginationOptions) => Promise<PaginatedResponse<T>>, options: PaginateAllOptions<T> = {},): Promise<{ items: T[]; totalPages: number; stoppedEarly: boolean }> { const { pageSize = 100, maxItems, onPage, stopWhen } = options
const allItems: T[] = [] let nextToken: string | undefined let pageNumber = 0 let stoppedEarly = false
do { pageNumber++
// Fetch the next page const response = await fetchPage({ limit: pageSize, next: nextToken, })
// Process items and check for early termination for (const item of response.items) { // Check stop condition if (stopWhen && stopWhen(item, allItems.length)) { stoppedEarly = true break }
allItems.push(item)
// Check max items limit if (maxItems && allItems.length >= maxItems) { stoppedEarly = true break } }
// Call page callback if provided if (onPage) { const continueIteration = onPage(response.items, pageNumber) if (continueIteration === false) { stoppedEarly = true break } }
// Check if we should stop if (stoppedEarly) { break }
nextToken = response.nextToken } while (nextToken)
return { items: allItems, totalPages: pageNumber, stoppedEarly }}
async function main() { printHeader('Pagination Example')
// Create clients const indexer = createIndexerClient() const algorand = createAlgorandClient()
// ========================================================================= // Step 1: Get a funded account and create some test data // ========================================================================= printStep(1, 'Setting up test data for pagination')
let senderAddress: string let senderAccount: Awaited<ReturnType<typeof algorand.account.kmd.getLocalNetDispenserAccount>>
try { senderAccount = await algorand.account.kmd.getLocalNetDispenserAccount() algorand.setSignerFromAccount(senderAccount) senderAddress = senderAccount.addr.toString() printSuccess(`Using dispenser account: ${shortenAddress(senderAddress)}`)
// Create several random accounts and send them funds to generate transactions printInfo('Creating test transactions for pagination demo...') const receiverAccounts: string[] = []
for (let i = 0; i < 5; i++) { const receiver = algorand.account.random() receiverAccounts.push(receiver.addr.toString())
await algorand.send.payment({ sender: senderAccount.addr, receiver: receiver.addr, amount: algo(1), }) }
printSuccess(`Created ${receiverAccounts.length} payment transactions`)
// Create a test asset printInfo('Creating test asset...') const assetResult = await algorand.send.assetCreate({ sender: senderAccount.addr, total: 1_000_000n, decimals: 0, assetName: 'PaginationTestToken', unitName: 'PAGE', }) printSuccess(`Created asset: PaginationTestToken (ID: ${assetResult.assetId})`)
// Wait for indexer to catch up printInfo('Waiting for indexer to index transactions...') await new Promise((resolve) => setTimeout(resolve, 3000)) printInfo('') } catch (error) { printError(`Failed to set up test data: ${error instanceof Error ? error.message : String(error)}`) printInfo('') printInfo('Make sure LocalNet is running: algokit localnet start') printInfo('If issues persist, try: algokit localnet reset') return }
// ========================================================================= // Step 2: Demonstrate pagination with searchForTransactions() // ========================================================================= printStep(2, 'Paginating through searchForTransactions()')
try { printInfo('Using the generic paginateAll helper to iterate through all transactions...') printInfo('Settings: pageSize=2 (small for demo purposes)') printInfo('')
const transactionResult = await paginateAll( async (opts) => { const result = await indexer.searchForTransactions({ limit: opts.limit, next: opts.next, }) return { items: result.transactions, nextToken: result.nextToken, currentRound: result.currentRound, } }, { pageSize: 2, // Small page size to demonstrate pagination maxItems: 10, // Limit to 10 for demo onPage: (items, pageNumber) => { printInfo(` Page ${pageNumber}: Retrieved ${items.length} transaction(s)`) }, }, )
printSuccess(`Total transactions fetched: ${transactionResult.items.length}`) printInfo(`Total pages fetched: ${transactionResult.totalPages}`) printInfo(`Stopped early (hit maxItems): ${transactionResult.stoppedEarly}`) printInfo('')
// Display first few transactions if (transactionResult.items.length > 0) { printInfo('First 3 transactions:') for (const tx of transactionResult.items.slice(0, 3)) { printInfo(` - ${tx.id ? shortenAddress(tx.id, 8, 6) : 'N/A'}: ${tx.txType}`) } } } catch (error) { printError(`searchForTransactions pagination failed: ${error instanceof Error ? error.message : String(error)}`) }
// ========================================================================= // Step 3: Demonstrate pagination with searchForAccounts() // ========================================================================= printStep(3, 'Paginating through searchForAccounts()')
try { printInfo('Fetching all accounts with balance > 0 using pagination...') printInfo('Settings: pageSize=3') printInfo('')
const accountResult = await paginateAll( async (opts) => { const result = await indexer.searchForAccounts({ currencyGreaterThan: 0n, limit: opts.limit, next: opts.next, }) return { items: result.accounts, nextToken: result.nextToken, currentRound: result.currentRound, } }, { pageSize: 3, maxItems: 15, onPage: (items, pageNumber) => { printInfo(` Page ${pageNumber}: Retrieved ${items.length} account(s)`) }, }, )
printSuccess(`Total accounts fetched: ${accountResult.items.length}`) printInfo(`Total pages fetched: ${accountResult.totalPages}`) printInfo('')
// Display accounts with their balances if (accountResult.items.length > 0) { printInfo('Accounts found:') for (const account of accountResult.items.slice(0, 5)) { printInfo(` - ${shortenAddress(account.address)}: ${formatMicroAlgo(account.amount)}`) } if (accountResult.items.length > 5) { printInfo(` ... and ${accountResult.items.length - 5} more`) } } } catch (error) { printError(`searchForAccounts pagination failed: ${error instanceof Error ? error.message : String(error)}`) }
// ========================================================================= // Step 4: Demonstrate pagination with searchForAssets() // ========================================================================= printStep(4, 'Paginating through searchForAssets()')
try { printInfo('Fetching all assets using pagination...') printInfo('Settings: pageSize=2') printInfo('')
const assetResult = await paginateAll( async (opts) => { const result = await indexer.searchForAssets({ limit: opts.limit, next: opts.next, }) return { items: result.assets, nextToken: result.nextToken, currentRound: result.currentRound, } }, { pageSize: 2, maxItems: 10, onPage: (items, pageNumber) => { printInfo(` Page ${pageNumber}: Retrieved ${items.length} asset(s)`) }, }, )
printSuccess(`Total assets fetched: ${assetResult.items.length}`) printInfo(`Total pages fetched: ${assetResult.totalPages}`) printInfo('')
// Display assets if (assetResult.items.length > 0) { printInfo('Assets found:') for (const asset of assetResult.items.slice(0, 5)) { const name = asset.params.name ?? 'Unnamed' const unitName = asset.params.unitName ?? 'N/A' printInfo(` - ID ${asset.id}: ${name} (${unitName})`) } if (assetResult.items.length > 5) { printInfo(` ... and ${assetResult.items.length - 5} more`) } } else { printInfo('No assets found on LocalNet') } } catch (error) { printError(`searchForAssets pagination failed: ${error instanceof Error ? error.message : String(error)}`) }
// ========================================================================= // Step 5: Display total count of items across all pages // ========================================================================= printStep(5, 'Counting total items across all pages')
try { printInfo('Counting all transactions without fetching full data...') printInfo('')
let totalTransactions = 0 let pageCount = 0 let nextToken: string | undefined
// Simple counting loop using limit and next do { pageCount++ const result = await indexer.searchForTransactions({ limit: 100, // Use larger page size for counting next: nextToken, })
totalTransactions += result.transactions.length nextToken = result.nextToken
// Safety limit for demo if (pageCount >= 10) { printInfo(' (stopping after 10 pages for demo purposes)') break } } while (nextToken)
printSuccess(`Total transactions counted: ${totalTransactions}`) printInfo(`Pages scanned: ${pageCount}`) printInfo('')
// Also count accounts printInfo('Counting all accounts...') let totalAccounts = 0 pageCount = 0 nextToken = undefined
do { pageCount++ const result = await indexer.searchForAccounts({ currencyGreaterThan: 0n, limit: 100, next: nextToken, })
totalAccounts += result.accounts.length nextToken = result.nextToken
if (pageCount >= 10) break } while (nextToken)
printSuccess(`Total accounts with balance > 0: ${totalAccounts}`) printInfo(`Pages scanned: ${pageCount}`) } catch (error) { printError(`Counting failed: ${error instanceof Error ? error.message : String(error)}`) }
// ========================================================================= // Step 6: Demonstrate early termination when a condition is met // ========================================================================= printStep(6, 'Demonstrating early termination')
try { printInfo('Searching for transactions until we find a payment transaction...') printInfo('')
const earlyTermResult = await paginateAll( async (opts) => { const result = await indexer.searchForTransactions({ limit: opts.limit, next: opts.next, }) return { items: result.transactions, nextToken: result.nextToken, currentRound: result.currentRound, } }, { pageSize: 5, stopWhen: (tx, index) => { // Stop when we find a payment transaction if (tx.txType === 'pay') { printInfo(` Found payment transaction at index ${index}: ${tx.id ? shortenAddress(tx.id, 8, 6) : 'N/A'}`) return true } return false }, onPage: (items, pageNumber) => { printInfo(` Page ${pageNumber}: Checking ${items.length} transaction(s)...`) }, }, )
printSuccess(`Stopped early: ${earlyTermResult.stoppedEarly}`) printInfo(`Total transactions before stopping: ${earlyTermResult.items.length}`) printInfo(`Pages checked: ${earlyTermResult.totalPages}`) printInfo('')
// Another example: stop after finding an account with specific balance printInfo('Searching for an account with balance > 1000 ALGO...')
const accountSearchResult = await paginateAll( async (opts) => { const result = await indexer.searchForAccounts({ currencyGreaterThan: 0n, limit: opts.limit, next: opts.next, }) return { items: result.accounts, nextToken: result.nextToken, currentRound: result.currentRound, } }, { pageSize: 5, stopWhen: (account) => { // Stop when we find an account with > 1000 ALGO (1,000,000,000 microAlgos) if (account.amount > 1_000_000_000_000n) { printInfo( ` Found whale account: ${shortenAddress(account.address)} with ${formatMicroAlgo(account.amount)}`, ) return true } return false }, }, )
if (accountSearchResult.stoppedEarly) { printSuccess('Found an account with > 1000 ALGO!') } else { printInfo('No account found with > 1000 ALGO (searched all accounts)') } } catch (error) { printError(`Early termination demo failed: ${error instanceof Error ? error.message : String(error)}`) }
// ========================================================================= // Step 7: Handle the case where there are no results // ========================================================================= printStep(7, 'Handling empty results')
try { printInfo('Searching for assets with a name that does not exist...') printInfo('')
const emptyResult = await paginateAll( async (opts) => { const result = await indexer.searchForAssets({ name: 'ThisAssetNameShouldNotExist12345', limit: opts.limit, next: opts.next, }) return { items: result.assets, nextToken: result.nextToken, currentRound: result.currentRound, } }, { pageSize: 10, onPage: (items, pageNumber) => { printInfo(` Page ${pageNumber}: Retrieved ${items.length} item(s)`) }, }, )
if (emptyResult.items.length === 0) { printSuccess('Correctly handled empty results (no assets found)') printInfo(`Total pages: ${emptyResult.totalPages}`) printInfo('Note: Empty results return an empty array, not an error') } else { printInfo(`Unexpectedly found ${emptyResult.items.length} asset(s)`) } printInfo('')
// Also demonstrate with accounts printInfo('Searching for accounts with impossibly high balance...') const emptyAccountResult = await paginateAll( async (opts) => { // Search for accounts with balance > max supply (would never exist) const result = await indexer.searchForAccounts({ currencyGreaterThan: 10_000_000_000_000_000n, // > 10 billion ALGO limit: opts.limit, next: opts.next, }) return { items: result.accounts, nextToken: result.nextToken, currentRound: result.currentRound, } }, { pageSize: 10 }, )
if (emptyAccountResult.items.length === 0) { printSuccess('Correctly handled empty results (no accounts with such high balance)') } else { printInfo(`Found ${emptyAccountResult.items.length} account(s)`) } } catch (error) { printError(`Empty results handling failed: ${error instanceof Error ? error.message : String(error)}`) }
// ========================================================================= // Step 8: Manual pagination without helper // ========================================================================= printStep(8, 'Manual pagination pattern (without helper)')
try { printInfo('Sometimes you may want to control pagination manually...') printInfo('')
// Manual pagination loop let allTransactions: Awaited<ReturnType<typeof indexer.searchForTransactions>>['transactions'] = [] let nextToken: string | undefined let pageNum = 0
do { pageNum++ const page = await indexer.searchForTransactions({ limit: 3, next: nextToken, })
allTransactions = allTransactions.concat(page.transactions) nextToken = page.nextToken
printInfo(` Page ${pageNum}: ${page.transactions.length} transactions (total: ${allTransactions.length})`)
// Limit for demo if (pageNum >= 3) { printInfo(' (stopping after 3 pages for demo)') break } } while (nextToken)
printSuccess(`Manual pagination complete: ${allTransactions.length} transactions in ${pageNum} pages`) printInfo('')
printInfo('Key pagination fields:') printInfo(' - limit: Maximum items per page (request parameter)') printInfo(' - nextToken: Token from response to fetch next page') printInfo(' - When nextToken is undefined/missing, no more pages exist') } catch (error) { printError(`Manual pagination failed: ${error instanceof Error ? error.message : String(error)}`) }
// ========================================================================= // Summary // ========================================================================= printHeader('Summary') printInfo('This example demonstrated pagination patterns for indexer endpoints:') printInfo('') printInfo('Pagination basics:') printInfo(' - Use `limit` parameter to control page size') printInfo(' - Use `next` parameter with `nextToken` from response to get next page') printInfo(' - When `nextToken` is undefined, there are no more pages') printInfo('') printInfo('Generic pagination helper (paginateAll):') printInfo(' - Reusable across all paginated endpoints') printInfo(' - Supports pageSize, maxItems limits') printInfo(' - Supports onPage callback for progress tracking') printInfo(' - Supports stopWhen condition for early termination') printInfo('') printInfo('Endpoints demonstrated:') printInfo(' - searchForTransactions() - paginate through transactions') printInfo(' - searchForAccounts() - paginate through accounts') printInfo(' - searchForAssets() - paginate through assets') printInfo('') printInfo('Best practices:') printInfo(' - Use larger page sizes (50-100) for production to reduce API calls') printInfo(' - Implement maxItems limit to prevent unbounded queries') printInfo(' - Use early termination when searching for specific items') printInfo(' - Handle empty results gracefully (empty array, not error)')}
main().catch((error) => { console.error('Fatal error:', error) process.exit(1)})