Skip to content

Cursor Pagination Guide ​

Bagisto GraphQL API uses cursor-based pagination for efficiently handling large datasets. This guide explains how to implement pagination in your applications.

Why Cursor Pagination? ​

Cursor-based pagination offers several advantages over offset-based pagination:

  • Consistency - Eliminates issues with data changing between requests
  • Performance - Efficient for large datasets
  • Infinite Scrolling - Perfect for mobile apps and dynamic feeds
  • Predictability - Cursors remain valid even if data is added/removed

Pagination Parameters ​

first ​

Retrieves the first N items from the collection.

graphql
query {
  products(first: 10) {
    edges {
      node {
        id
        name
      }
    }
  }
}

after ​

Returns items after a specific cursor. Used for forward pagination (next page).

graphql
query {
  products(first: 10, after: "cursor_value") {
    edges {
      node {
        id
        name
      }
    }
  }
}

last ​

Retrieves the last N items from the collection.

graphql
query {
  products(last: 10) {
    edges {
      node {
        id
        name
      }
    }
  }
}

before ​

Returns items before a specific cursor. Used for backward pagination (previous page).

graphql
query {
  products(last: 10, before: "cursor_value") {
    edges {
      node {
        id
        name
      }
    }
  }
}

totalItems ​

Returns the total count of items in the collection.

graphql
query {
  products(first: 10) {
    totalItems
    edges {
      node {
        id
        name
      }
    }
  }
}

Response Structure ​

Paginated responses have a standard structure:

json
{
  "data": {
    "products": {
      "edges": [
        {
          "node": {
            "id": "1",
            "name": "Product Name"
          },
          "cursor": "Y3Vyc29yOjE="
        }
      ],
      "pageInfo": {
        "hasNextPage": true,
        "hasPreviousPage": false,
        "startCursor": "Y3Vyc29yOjE=",
        "endCursor": "Y3Vyc29yOjEw"
      },
      "totalItems": 150
    }
  }
}

Response Fields ​

FieldTypeDescription
edgesArrayCollection of items with their cursors
nodeObjectThe actual item data
cursorStringEncoded cursor for this item
pageInfoObjectPagination metadata
hasNextPageBooleanWhether more items exist after current page
hasPreviousPageBooleanWhether items exist before current page
startCursorStringCursor of first item in current page
endCursorStringCursor of last item in current page
totalItemsIntegerTotal number of items in collection

Common Pagination Patterns ​

Forward Pagination (Next Page) ​

Load initial page:

graphql
query {
  products(first: 10) {
    edges {
      node {
        id
        name
      }
      cursor
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

Load next page using the endCursor:

graphql
query {
  products(first: 10, after: "Y3Vyc29yOjEw") {
    edges {
      node {
        id
        name
      }
      cursor
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

Backward Pagination (Previous Page) ​

Load previous page using before and last:

graphql
query {
  products(last: 10, before: "Y3Vyc29yOjEw") {
    edges {
      node {
        id
        name
      }
      cursor
    }
    pageInfo {
      hasPreviousPage
      startCursor
    }
  }
}

Infinite Scrolling ​

Implement infinite scrolling for dynamic feeds:

javascript
let hasMore = true;
let endCursor = null;

async function loadMore() {
  const query = `
    query {
      products(first: 20, ${endCursor ? `after: "${endCursor}"` : ''}) {
        edges {
          node {
            id
            name
            price
          }
          cursor
        }
        pageInfo {
          hasNextPage
          endCursor
        }
      }
    }
  `;

  const response = await fetch('/api/graphql', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query })
  });

  const { data } = await response.json();
  const { edges, pageInfo } = data.products;

  // Add items to DOM
  edges.forEach(({ node }) => {
    appendProductToUI(node);
  });

  // Update pagination state
  hasMore = pageInfo.hasNextPage;
  endCursor = pageInfo.endCursor;
}

// Call loadMore() when user scrolls to bottom
window.addEventListener('scroll', () => {
  if (window.innerHeight + window.scrollY >= document.body.offsetHeight && hasMore) {
    loadMore();
  }
});

Search with Pagination ​

Combine search filters with pagination:

graphql
query {
  products(
    first: 20
    filter: {
      search: "laptop"
      minPrice: 500
      maxPrice: 2000
    }
  ) {
    edges {
      node {
        id
        name
        price
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
    totalItems
  }
}

Get Total Count ​

Get pagination metadata including total items:

graphql
query {
  products(first: 1) {
    totalItems
    pageInfo {
      hasNextPage
    }
  }
}

Pagination with Multiple Collections ​

When working with nested collections, apply pagination at each level:

graphql
query {
  products(first: 10) {
    edges {
      node {
        id
        name
        reviews(first: 5) {
          edges {
            node {
              id
              title
              rating
            }
          }
          pageInfo {
            hasNextPage
          }
        }
      }
    }
    pageInfo {
      hasNextPage
    }
  }
}

Best Practices ​

1. Choose Appropriate Page Size ​

graphql
# Good - reasonable page size
query {
  products(first: 20) { ... }
}

# Avoid - too small, excessive requests
query {
  products(first: 1) { ... }
}

# Avoid - too large, performance issues
query {
  products(first: 1000) { ... }
}

2. Always Check hasNextPage ​

javascript
if (pageInfo.hasNextPage) {
  // Load more button or trigger
}

3. Store Cursors for Navigation ​

javascript
// Store cursor for next/previous navigation
const nextCursor = pageInfo.endCursor;
const prevCursor = pageInfo.startCursor;

4. Handle Empty Results ​

javascript
if (edges.length === 0) {
  displayEmptyState();
} else {
  renderItems(edges);
}

5. Implement Loading States ​

javascript
let isLoading = false;

async function loadMore() {
  if (isLoading) return;
  isLoading = true;
  
  try {
    // Fetch data
  } finally {
    isLoading = false;
  }
}

Performance Considerations ​

Selective Field Queries ​

Request only needed fields:

graphql
# Good - specific fields
query {
  products(first: 20) {
    edges {
      node {
        id
        name
        price
      }
    }
  }
}

# Avoid - unnecessary fields
query {
  products(first: 20) {
    edges {
      node {
        __typename
        id
        name
        price
        description
        longDescription
        images { ... }
        relatedProducts { ... }
        variants { ... }
      }
    }
  }
}

Caching with Pagination ​

Implement client-side caching:

javascript
const cache = new Map();

async function getProducts(first, after) {
  const key = `products:${first}:${after || 'start'}`;
  
  if (cache.has(key)) {
    return cache.get(key);
  }

  const data = await fetchProducts(first, after);
  cache.set(key, data);
  return data;
}

Error Handling ​

Handle pagination errors gracefully:

javascript
async function getPagedProducts(cursor) {
  try {
    const response = await fetch('/api/graphql', {
      method: 'POST',
      body: JSON.stringify({ query: paginationQuery(cursor) })
    });

    const { data, errors } = await response.json();

    if (errors) {
      console.error('GraphQL Error:', errors);
      // Handle error - reset pagination or show message
      return null;
    }

    return data.products;

  } catch (error) {
    console.error('Network Error:', error);
    // Handle network error
    return null;
  }
}

Collection-Specific Pagination ​

Different collections support pagination. Here are common examples:

Products ​

graphql
query {
  products(first: 20) { ... }
}

Categories ​

graphql
query {
  categories(first: 20) { ... }
}

Orders ​

graphql
query {
  orders(first: 20) { ... }
}

Reviews ​

graphql
query {
  productReviews(first: 20) { ... }
}

Customers (Admin API) ​

graphql
query {
  customers(first: 20) { ... }
}

Migration from Offset Pagination ​

If migrating from offset-based pagination:

Before (Offset) ​

graphql
query {
  products(limit: 20, offset: 0) {
    items { ... }
    total
  }
}

After (Cursor) ​

graphql
query {
  products(first: 20) {
    edges {
      node { ... }
      cursor
    }
    totalItems
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

Troubleshooting ​

Cursor Expired ​

Issue: "Invalid cursor" error

Solution: Re-fetch from the beginning with fresh cursors

javascript
// Reset to first page
endCursor = null;
await loadMore();

Inconsistent Results ​

Issue: Items missing or duplicated between pages

Solution: Use consistent sorting with pagination:

graphql
query {
  products(first: 20, sort: { field: "createdAt", direction: "DESC" }) {
    ...
  }
}

Performance Issues ​

Issue: Pagination queries are slow

Solution:

  • Reduce page size
  • Use specific field queries
  • Add proper indexes on sorted fields
  • Implement caching

Related Topics:

Released under the MIT License.