Features

Pagination

rezo.paginate() is an async generator that fetches pages from a paginated API one at a time. It auto-detects pagination via standard Link headers, or you can provide custom logic for cursor-based, offset-based, or any other pagination scheme.

Quick Start

import { Rezo } from 'rezo';

const rezo = new Rezo({ baseURL: 'https://api.example.com' });

// Auto-detect pagination via Link headers
for await (const page of rezo.paginate('/users')) {
  console.log('Page data:', page);
}

How It Works

On each iteration, paginate() makes a GET request and yields the page data. It then determines the next page URL:

  1. If you provide a getNextUrl function, it calls that with the response.
  2. Otherwise, it parses the Link header and follows the rel="next" URL.
  3. If neither produces a next URL, iteration stops.

Many APIs return pagination links in the Link header (GitHub, GitLab, Stripe, etc.):

Link: <https://api.example.com/users?page=2>; rel="next", <https://api.example.com/users?page=5>; rel="last"

Rezo parses this automatically:

for await (const users of rezo.paginate('/users')) {
  for (const user of users) {
    console.log(user.name);
  }
}

No configuration needed — if the server sends a Link header with rel="next", Rezo follows it.

Custom getNextUrl

For APIs that use cursors, tokens, or non-standard pagination, provide a getNextUrl function:

Cursor-Based

for await (const page of rezo.paginate('/items', {
  pagination: {
    getNextUrl: (response) => {
      const cursor = response.data.next_cursor;
      return cursor ? `/items?cursor=${cursor}` : null;
    }
  }
})) {
  console.log('Items:', page.results);
}

Offset-Based

let offset = 0;
const limit = 50;

for await (const page of rezo.paginate(`/records?offset=${offset}&limit=${limit}`, {
  pagination: {
    getNextUrl: (response) => {
      offset += limit;
      if (response.data.length < limit) return null; // Last page
      return `/records?offset=${offset}&limit=${limit}`;
    }
  }
})) {
  processRecords(page);
}

Token-Based

for await (const page of rezo.paginate('/events', {
  pagination: {
    getNextUrl: (response) => {
      const token = response.data.pagination?.next_token;
      return token ? `/events?page_token=${token}` : null;
    }
  }
})) {
  for (const event of page.events) {
    handleEvent(event);
  }
}

Using Response Headers

for await (const page of rezo.paginate('/data', {
  pagination: {
    getNextUrl: (response) => {
      return response.headers.get('x-next-page') || null;
    }
  }
})) {
  console.log(page);
}

Return null or undefined from getNextUrl to stop pagination.

Transform

By default, paginate() yields response.data from each page. Use transform to reshape or extract the data:

Extract Nested Results

const allUsers = [];

for await (const users of rezo.paginate('/users', {
  pagination: {
    transform: (response) => response.data.results
  }
})) {
  allUsers.push(...users);
}

console.log('Total users:', allUsers.length);

Include Response Metadata

for await (const page of rezo.paginate('/items', {
  pagination: {
    transform: (response) => ({
      items: response.data.items,
      total: response.data.total_count,
      status: response.status,
      rateLimit: response.headers.get('x-ratelimit-remaining')
    })
  }
})) {
  console.log(`${page.items.length} items (${page.total} total, rate limit: ${page.rateLimit})`);
}

Request Limits

Prevent runaway pagination with countLimit or requestLimit (they are aliases):

// Fetch at most 10 pages
for await (const page of rezo.paginate('/users', {
  pagination: {
    countLimit: 10
  }
})) {
  console.log(page);
}

// Same thing using requestLimit
for await (const page of rezo.paginate('/users', {
  pagination: {
    requestLimit: 10
  }
})) {
  console.log(page);
}

Without a limit, pagination continues until no next URL is found.

Collecting All Pages

async function fetchAll(url, options) {
  const allItems = [];
  for await (const page of rezo.paginate(url, {
    pagination: {
      transform: (r) => r.data.results,
      ...options
    }
  })) {
    allItems.push(...page);
  }
  return allItems;
}

const users = await fetchAll('/users');
const repos = await fetchAll('/repos', { countLimit: 5 });

With Request Options

Pass any standard request options alongside pagination:

for await (const page of rezo.paginate('/private/data', {
  headers: {
    'Authorization': 'Bearer token123',
    'Accept': 'application/json'
  },
  timeout: 10_000,
  pagination: {
    getNextUrl: (r) => r.data.next || null,
    countLimit: 20
  }
})) {
  process.stdout.write('.');
}

Full Example: GitHub API

const github = new Rezo({
  baseURL: 'https://api.github.com',
  headers: {
    'Authorization': `Bearer ${process.env.GITHUB_TOKEN}`,
    'Accept': 'application/vnd.github+json'
  }
});

// Fetch all repositories for a user (auto-detects Link header pagination)
const allRepos = [];

for await (const repos of github.paginate('/users/octocat/repos?per_page=100')) {
  allRepos.push(...repos);
  console.log(`Fetched ${allRepos.length} repos so far...`);
}

console.log('Total repos:', allRepos.length);

Full Example: Cursor-Based API

const api = new Rezo({ baseURL: 'https://api.example.com/v2' });

const allOrders = [];

for await (const batch of api.paginate('/orders', {
  pagination: {
    getNextUrl: (response) => {
      const { has_more, cursor } = response.data.pagination;
      return has_more ? `/orders?cursor=${cursor}&limit=100` : null;
    },
    transform: (response) => response.data.orders,
    requestLimit: 50 // Safety limit
  }
})) {
  allOrders.push(...batch);
  console.log(`Collected ${allOrders.length} orders`);
}