Efficient GraphQL Pagination: Cursor-Based & Limit-Offset

Muhaymin Bin Mehmood

Muhaymin Bin Mehmood

· 8 min read
Efficient GraphQL Pagination: Cursor-Based & Limit-Offset Banner Image
Efficient GraphQL Pagination: Cursor-Based & Limit-Offset Banner Image

Pagination is a cornerstone of data management in modern applications, especially when dealing with large datasets. Instead of overwhelming users and systems with massive data loads, pagination provides a way to fetch data in smaller, more manageable chunks. In this blog, we will explore how to implement pagination in GraphQL, focusing on its two primary strategies: cursor-based pagination and limit-offset pagination. Additionally, we’ll integrate these solutions into a front-end application using Apollo Client.

Table of Contents

  1. Why is Pagination Important?
  2. Types of Pagination
    • Cursor-Based Pagination
    • Limit-Offset Pagination
  3. Setting Up Pagination in Apollo Server
    • Project Setup
    • Schema Definition
    • Resolver Implementation
  4. Integrating Pagination with Apollo Client
    • Setting Up Apollo Client
    • Querying Paginated Data
    • Load More Pagination
    • Infinite Scrolling
  5. Conclusion
  6. Frequently Asked Questions (FAQs)

Why is Pagination Important?

Pagination is essential for:

  1. Performance Optimization: Fetching smaller datasets reduces server response time and memory usage.
  2. Scalability: Handles increasing datasets without degrading performance.
  3. Improved User Experience: Displays data incrementally, ensuring faster load times and smoother interactions.
  4. Bandwidth Efficiency: Reduces unnecessary data transfer, especially in mobile and low-bandwidth environments.

While GraphQL simplifies data querying, pagination ensures the system remains efficient and responsive as data grows.

Types of Pagination

GraphQL supports two main pagination strategies:

  • Cursor-Based Pagination:
    • Utilizes a unique identifier (cursor) to track the dataset's position.
    • Efficient for dynamic datasets where data might frequently change.
    • Suitable for social media feeds or real-time applications.
  • Limit-Offset Pagination:
    • Uses a numerical offset to determine the starting point in the dataset.
    • Simpler but less efficient with large or constantly changing datasets.
    • Commonly used in databases like SQL.

Setting Up GraphQL Pagination with Apollo Server

Before implementing pagination, let’s set up a basic Apollo Server to handle GraphQL queries.

Project Setup

Start with the following steps:

  • Initialize a new Node.js project.
  • Install necessary packages:
npm install apollo-server graphql
  • Create a file structure:
- src/
  - index.js
  - schema.js
  - resolvers.js

Cursor-Based Pagination

Cursor-based pagination is preferred for most modern applications because it handles dynamic datasets effectively. Let’s start with the schema:

Schema Definition

The schema defines the data structure for pagination, including PageInfo and PostConnection.

const { gql } = require('apollo-server');

const typeDefs = gql`
  type Post {
    id: ID!
    title: String!
    content: String!
  }

  type PageInfo {
    endCursor: String
    hasNextPage: Boolean!
  }

  type PostConnection {
    edges: [PostEdge]
    pageInfo: PageInfo!
  }

  type PostEdge {
    node: Post
  }

  type Query {
    posts(first: Int, after: String): PostConnection!
  }
`;

module.exports = typeDefs;

How Cursor-Based Pagination Works

  • first: Specifies how many items to fetch.
  • after: A cursor indicating where to start fetching data.

The PageInfo object ensures the client knows whether more data is available (hasNextPage) and provides the cursor for subsequent requests (endCursor).

Resolver Implementation

The resolver fetches data based on the first and after arguments:

const posts = [
  { id: '1', title: 'Post 1', content: 'Content 1' },
  { id: '2', title: 'Post 2', content: 'Content 2' },
  { id: '3', title: 'Post 3', content: 'Content 3' },
  // Add more posts for testing
];

const resolvers = {
  Query: {
    posts: (parent, { first = 5, after }, context, info) => {
      const startIndex = after
        ? posts.findIndex((post) => post.id === after) + 1
        : 0;

      const paginatedPosts = posts.slice(startIndex, startIndex + first);
      const endCursor = paginatedPosts.length
        ? paginatedPosts[paginatedPosts.length - 1].id
        : null;

      return {
        edges: paginatedPosts.map((post) => ({ node: post })),
        pageInfo: {
          endCursor,
          hasNextPage: startIndex + first < posts.length,
        },
      };
    },
  },
};

module.exports = resolvers;

This resolver:

  • Finds the starting point based on the cursor (after).
  • Slices the dataset to return the requested number of records (first).
  • Provides pagination metadata via PageInfo.

Limit-Offset Pagination

Limit-offset pagination is simpler but less effective for large datasets due to potential inconsistencies caused by shifting records.

Schema and Resolver

Update the resolver to use offset and limit:

const resolvers = {
  Query: {
    posts: (parent, { first = 5, offset = 0 }, context, info) => {
      const paginatedPosts = posts.slice(offset, offset + first);
      return {
        edges: paginatedPosts.map((post) => ({ node: post })),
        pageInfo: {
          hasNextPage: offset + first < posts.length,
          endCursor: paginatedPosts[paginatedPosts.length - 1]?.id || null,
        },
      };
    },
  },
};

The simplicity of this method makes it a good choice for static or small datasets.

Integrating Pagination with Apollo Client

Now that the server is ready, let’s integrate pagination on the frontend using Apollo Client.

Setting Up Apollo Client

Install the Apollo Client and React dependencies:

npm install @apollo/client graphql

Querying Paginated Data

Create a query for paginated posts:

import { gql } from '@apollo/client';

export const GET_POSTS = gql`
  query GetPosts($first: Int, $after: String) {
    posts(first: $first, after: $after) {
      edges {
        node {
          id
          title
          content
        }
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
`;

Load More Pagination

Implement a "Load More" feature to fetch additional records:

import React, { useState } from 'react';
import { useQuery } from '@apollo/client';
import { GET_POSTS } from './queries';

const PostList = () => {
  const [cursor, setCursor] = useState(null);
  const { data, loading, fetchMore } = useQuery(GET_POSTS, {
    variables: { first: 5, after: cursor },
  });

  const handleLoadMore = () => {
    if (data.posts.pageInfo.hasNextPage) {
      fetchMore({
        variables: { after: data.posts.pageInfo.endCursor },
      });
    }
  };

  if (loading) return <p>Loading...</p>;

  return (
    <div>
      <ul>
        {data.posts.edges.map(({ node }) => (
          <li key={node.id}>{node.title}</li>
        ))}
      </ul>
      {data.posts.pageInfo.hasNextPage && (
        <button onClick={handleLoadMore}>Load More</button>
      )}
    </div>
  );
};

export default PostList;

This implementation:

  • Fetches additional data using the fetchMore function.
  • Updates the endCursor for the next query.

Infinite Scrolling

For infinite scrolling, detect when the user scrolls near the bottom of the page and trigger fetchMore:

import React, { useEffect } from 'react';

const InfiniteScroll = ({ fetchMore }) => {
  const handleScroll = () => {
    if (
      window.innerHeight + document.documentElement.scrollTop ===
      document.documentElement.offsetHeight
    ) {
      fetchMore();
    }
  };

  useEffect(() => {
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);

  return null;
};

Conclusion

Pagination is an indispensable technique for handling large datasets in GraphQL applications. While cursor-based pagination is ideal for dynamic datasets, limit-offset pagination works well for simpler scenarios. With the integration of Apollo Client, you can create efficient and user-friendly experiences, whether through "Load More" buttons or infinite scrolling.

By mastering these strategies, you can ensure your applications remain scalable, efficient, and user-friendly.

Frequently Asked Questions (FAQs)

1. What is pagination in GraphQL?

Pagination is a technique used to fetch data in smaller, more manageable chunks rather than loading an entire dataset at once. In GraphQL, it can be implemented using arguments like first, after, limit, and offset.

2. What is the difference between cursor-based and limit-offset pagination?

  • Cursor-Based Pagination: Tracks the position in the dataset using a cursor (e.g., unique identifiers). It’s more efficient for dynamic or large datasets.
  • Limit-Offset Pagination: Fetches data starting from a numerical offset. It’s simpler but less efficient for large datasets due to potential inconsistencies when records are added or removed.

3. Why is cursor-based pagination recommended over limit-offset pagination?

Cursor-based pagination is preferred because:

  • It handles dynamic datasets where data might frequently change.
  • It avoids issues with duplicate or missing records, which can occur with offset-based pagination.

4. How do I implement pagination in Apollo Server?

  • Define a schema with PageInfo and edges for cursor-based pagination.
  • Write resolvers to fetch data based on first (limit) and after (cursor) arguments.
  • Return metadata like hasNextPage and endCursor for client-side navigation.

5. How do I implement pagination in Apollo Client?

  • Write a GraphQL query with arguments for pagination (first and after for cursor-based pagination).
  • Use the fetchMore method to load additional data incrementally.
  • Optionally implement infinite scrolling for a seamless user experience.

6. Which pagination strategy should I choose for my project?

  • Use cursor-based pagination for dynamic or large datasets, such as social media feeds or live updates.
  • Use limit-offset pagination for static or smaller datasets, such as simple product catalogs.

7. How can I optimize the performance of pagination?

  • Limit the number of records fetched in each request to reduce server load.
  • Use indexed fields for cursor-based queries to ensure faster lookups.
  • Avoid fetching unnecessary fields in your GraphQL queries.

8. What are some common challenges with pagination?

  • Managing edge cases, such as empty datasets or reaching the end of available records.
  • Handling changes in data while paginating, especially in dynamic datasets.
  • Optimizing backend queries to avoid performance bottlenecks.

9. Can I combine pagination with filtering and sorting?

Yes, GraphQL supports combining pagination with filtering and sorting. You can include arguments like filterBy or orderBy in your queries to refine the results before applying pagination.

10. What tools or libraries can help with GraphQL pagination?

  • Apollo Server: For defining and implementing GraphQL resolvers.
  • Apollo Client: For querying paginated data and managing client-side state.
  • Relay: A GraphQL client library that supports cursor-based pagination out of the box.
Muhaymin Bin Mehmood

About Muhaymin Bin Mehmood

Front-end Developer skilled in the MERN stack, experienced in web and mobile development. Proficient in React.js, Node.js, and Express.js, with a focus on client interactions, sales support, and high-performance applications.

Join our newsletter

Subscribe now to our newsletter for regular updates.

Copyright © 2025 Mbloging. All rights reserved.