Skip to content
Algorand Developer Portal

Pagination

← Back to Indexer Client

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.

  • LocalNet running (via algokit localnet start)

From the repository root:

Terminal window
cd examples
npm run example indexer_client/16-pagination.ts

View source on GitHub

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);
});