paginate

April 11, 2026 ยท View on GitHub

paginate(input, options?)

Paginate through API responses using async iteration. By default, it automatically follows RFC 5988 Link headers with rel="next".

Returns an async iterator that yields items from each page.

Note

This function does not check response status codes. If you need error handling for non-2xx responses, wrap fetch with withHttpError() or handle errors in your transform function.

Important

When pagination crosses to a different origin, inherited request headers are cleared before the next request is built. Then wrappers like withHeaders() run again for that next page. If you intentionally need extra per-page headers on the new origin, return them explicitly from pagination.paginate.

Note

Later pages are new requests. If your fetchFunction uses withHeaders(), its defaults still apply on each page. Function-based defaults are re-resolved for each page instead of being frozen from the first page.

import {paginate} from 'fetch-extras';

// Basic usage with Link headers (GitHub API)
for await (const commit of paginate('https://api.github.com/repos/sindresorhus/ky/commits')) {
	console.log(commit.sha);
}

With error handling for non-2xx responses:

import {paginate} from 'fetch-extras';

for await (const item of paginate('https://api.example.com/items', {
	pagination: {
		transform: async (response) => {
			if (!response.ok) {
				throw new Error(`HTTP ${response.status}`);
			}
			return response.json();
		},
		countLimit: 100,
		backoff: 1000
	}
})) {
	console.log(item);
}

options

Type: object

pagination

Type: object

transform

Type: (response: Response) => Promise<unknown[]>
Default: response => response.json()

Transform the response into an array of items.

for await (const user of paginate('https://api.example.com/users', {
	pagination: {
		transform: async response => {
			const data = await response.json();
			return data.users; // Extract from nested property
		}
	}
})) {
	console.log(user);
}
paginate (option)

Type: (data: {response, currentUrl, currentItems, allItems}) => Promise<PaginationNextPage | false>
Default: Parses RFC 5988 Link header

Determine the next page to fetch. Return an object with fetch options for the next request, or false to stop pagination.

Important

The response body has already been consumed by the transform function. Do NOT call response.json() or other body methods here. Extract pagination info from headers, the URL, or share data from the transform function through closure.

Note

Returning headers replaces all inherited headers, consistent with standard Fetch API behavior. When the next page crosses to a different origin, inherited request headers are already cleared before your returned headers are applied. Then wrappers such as withHeaders() run again for that next page. If you need to add headers while keeping existing ones, read them from the response and include them in the returned object. Setting body to undefined will strip body-related headers (Content-Type, Content-Length, etc.) from the request, consistent with HTTP semantics for bodyless requests. Function-based withHeaders() defaults are still resolved per page after that merge.

// Cursor-based pagination using headers (recommended)
for await (const item of paginate('https://api.example.com/items', {
	pagination: {
		paginate: ({response}) => {
			const cursor = response.headers.get('X-Next-Cursor');
			return cursor
				? {url: new URL(`https://api.example.com/items?cursor=${cursor}`)}
				: false;
		}
	}
})) {
	console.log(item);
}
// Sharing data between transform and paginate via closure
let nextCursor;

for await (const item of paginate('https://api.example.com/items', {
	pagination: {
		transform: async (response) => {
			const data = await response.json();
			nextCursor = data.nextCursor;
			return data.items;
		},
		paginate: () => {
			return nextCursor
				? {url: new URL(`https://api.example.com/items?cursor=${nextCursor}`)}
				: false;
		}
	}
})) {
	console.log(item);
}
filter

Type: (data: {item, currentItems, allItems}) => boolean
Default: () => true

Filter items before yielding them.

// Only get active users
for await (const user of paginate('https://api.example.com/users', {
	pagination: {
		filter: ({item}) => item.status === 'active'
	}
})) {
	console.log(user);
}
shouldContinue

Type: (data: {item, currentItems, allItems}) => boolean
Default: () => true

Check if pagination should continue after yielding an item. This is called after filter returns true. Useful for stopping pagination based on item values.

// Stop when we reach items older than one week
const oneWeekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);

for await (const commit of paginate('https://api.github.com/repos/user/repo/commits', {
	pagination: {
		shouldContinue: ({item}) => new Date(item.date).getTime() >= oneWeekAgo
	}
})) {
	console.log(commit);
}
countLimit

Type: number
Default: Infinity

Maximum number of items to yield.

const items = await paginate.all('https://api.example.com/items', {
	pagination: {
		countLimit: 100 // Stop after 100 items
	}
});
requestLimit

Type: number
Default: 10000

Maximum number of requests to make. This prevents infinite loops if your paginate function has bugs. Ensure your paginate function eventually returns false or the iteration will continue until this limit is reached.

backoff

Type: number
Default: 0

Delay in milliseconds between requests. Useful for rate limiting.

for await (const item of paginate('https://api.example.com/items', {
	pagination: {
		backoff: 1000 // Wait 1 second between requests
	}
})) {
	console.log(item);
}
stackAllItems

Type: boolean
Default: false

Whether to keep all yielded items in memory. When true, the allItems array passed to callbacks will contain all previously yielded items. When false, allItems will always be empty to save memory.

fetchFunction

Type: (input: RequestInfo | URL, init?: any) => Promise<Response>
Default: globalThis.fetch

Custom fetch function to use for requests. This allows you to use a custom fetch implementation, such as ky, or a fetch function wrapped with withHttpError or withTimeout.

import {paginate} from 'fetch-extras';
import ky from 'ky';

const url = 'https://api.github.com/repos/sindresorhus/ky/commits';

for await (const commit of paginate(url, {fetchFunction: ky})) {
	console.log(commit.sha);
}

paginate.all(input, options?)

Get all paginated items as an array. This is a convenience method that collects all items into memory. For large datasets, prefer using the async iterator directly.

import {paginate} from 'fetch-extras';

const commits = await paginate.all('https://api.github.com/repos/sindresorhus/ky/commits', {
	pagination: {
		countLimit: 50
	}
});

console.log(`Fetched ${commits.length} commits`);