← Back to list

Mastering GraphQL Pagination: From Basics to Advanced Cursor-Based Strategies

Hey there, GraphQL adventurers! If you’ve mastered the core concepts from my previous post (check it out here if you haven’t…

Marium Shahid · 2025-08-24 19:34 · 0 claps · 5.4 min read
#gql #api-architecture #pagination #software-development #web-development
Open on Medium ↗
Wiki topics: 🌐 · Web Development 🏛️ · Architecture

Mastering GraphQL Pagination: From Basics to Advanced Cursor-Based Strategies

Hey there, GraphQL adventurers! If you’ve mastered the core concepts from my previous post (check it out here if you haven’t: https://medium.com/@mariumgoraya13/mastering-graphql-a-comprehensive-guide-to-its-core-concepts-915cd25b0e51),,) you’re ready to level up. Today, we’re diving into one of the most crucial advanced topics: pagination.

Pagination is all about handling lists of data efficiently — think scrolling through your social feed or browsing search results. In GraphQL, it’s not just about fetching data; it’s about doing it smartly to avoid performance pitfalls. We’ll explore simple approaches, then build up to the powerful cursor-based model, with plenty of engaging examples along the way. No servers or languages here — just pure concepts to make you a pagination pro. Let’s paginate our way through this!

Why Pagination Matters in GraphQL

Imagine querying for a user’s friends in a massive social network. Without pagination, you might fetch thousands of records at once — crushing your app’s performance and user experience. Pagination lets clients request data in bite-sized chunks, like “give me the first 10 friends, then the next 10.”

GraphQL offers flexibility here, but not all methods are equal. We’ll cover:

  • Offset-based: Simple but flawed for large datasets.
  • ID-based: Better, using object IDs for navigation.
  • Cursor-based: The gold standard-efficient, flexible, and future-proof.

We’ll use a Star Wars-themed schema (inspired by the official GraphQL docs) for examples, where characters have friends lists.

Starting Simple: Plurals and Basic Lists

The easiest way to expose lists in GraphQL is with plural types — fields that return arrays of objects.

type Character { id: ID! name: String! friends: [Character] }

Query for R2-D2’s friends:

{ hero(id: "2001") { # R2-D2’s ID name friends { name } } }

Response:

{ "data": { "hero": { "name": "R2-D2", "friends": [ { "name": "Luke Skywalker" }, { "name": "Han Solo" }, { "name": "Leia Organa" } ] } } }

This is straightforward — like grabbing the whole menu at once. But what if there are 1,000 friends? Your app slows down, and bandwidth wastes away. Enter slicing!

Slicing: Limiting the List Size

To avoid fetching everything, add arguments like first: Int to slice the list.

Updated schema:

type Character { id: ID! name: String! friends(first: Int): [Character] }

Query for the first two friends:

{ hero(id: "2001") { name friends(first: 2) { name } } }

Response:

{ "data": { "hero": { "name": "R2-D2", "friends": [ { "name": "Luke Skywalker" }, { "name": "Han Solo" } ] } } }

Analogy: It’s like ordering appetizers first instead of the full buffet. But to “paginate” further (get the next two), we need a way to continue from where we left off. That’s where pagination strategies shine.

Exploring Pagination Styles: Offset, ID, and Cursor

Now, let’s compare approaches to fetching the “next” page.

Offset-Based Pagination: The Classic (But Risky) Approach

Add an offset: Int argument to skip initial items.

friends(first: Int, offset: Int): [Character]

First page (first 2, offset 0):

{ hero(id: "2001") { friends(first: 2, offset: 0) { name } } }

Response: Luke and Han.

Second page (first 2, offset 2):

{ hero(id: "2001") { friends(first: 2, offset: 2) { name } } }

Response: Leia (and maybe more if the list grows).

Pros:

  • Simple math, offset = page * limit.

Cons:

  • Performance killer for large lists (e.g., databases scan from the start each time).
  • Unstable: If a new friend is added mid-pagination, offsets shift, causing duplicates or skips.
  • Security risks: Exposes total count indirectly, potentially leading to DOS attacks.

Real-world pitfall: In a live app, if Han unfriends R2-D2 between requests, your offsets break. Not ideal for dynamic data!

ID-Based Pagination: Using Object IDs for Navigation

Improve by using the last item’s ID as a starting point.

friends(first: Int, after: ID): [Character]

Assume friends are sorted by ID.

First page:

{ hero(id: "2001") { friends(first: 2) { id name } } }

Response:

{ "friends": [ { "id": "1000", "name": "Luke Skywalker" }, { "id": "1001", "name": "Han Solo" } ] }

Second page (after Han’s ID):

{ hero(id: "2001") { friends(first: 2, after: "1001") { id name } } }

Response: Next friends starting after ID 1001.

Pros: Avoids full scans; stable if IDs are sequential.

Cons: Assumes sortable IDs; deletions can create gaps; not opaque, clients might guess patterns

Cursor-Based Pagination: The Flexible Powerhouse

Cursors are opaque strings (often base64-encoded) representing a position in the list. They’re not IDs or offsets — they’re black-box tokens from the server.

Schema introduces edges and connections for richness.

Basic idea: Wrap list items in “edges” with cursors.

type FriendsConnection { edges: [FriendsEdge] }

type FriendsEdge { cursor: String! node: Character }

type Character { id: ID! name: String! friendsConnection(first: Int, after: String): FriendsConnection }

First page (first: 2)

{ hero(id: "2001") { name friendsConnection(first: 2) { edges { cursor node { name } } } } }

Response:

{ "data": { "hero": { "name": "R2-D2", "friendsConnection": { "edges": [ { "cursor": "Y3Vyc29yMQ==", "node": { "name": "Luke Skywalker" } }, { "cursor": "Y3Vyc29yMg==", "node": { "name": "Han Solo" } } ] } } } }

Second page (after the second cursor):

{ hero(id: "2001") { friendsConnection(first: 2, after: "Y3Vyc29yMg==") { edges { cursor node { name } } } } }

Response: Leia and beyond (if more exist).

Pros:

  • Opaque cursors hide implementation (e.g., could be offset, ID, or timestamp).
  • Stable: Handles inserts/deletes gracefully.
  • Extensible: Add metadata like total count or page info.

Cons:

  • Requires more schema complexity (edges, connections).

Enhancing Connections: End-of-List and Metadata

Cursors alone aren’t enough. How do you know you’ve hit the end? Add a connection object with metadata.

type FriendsConnection { edges: [FriendsEdge] totalCount: Int pageInfo: PageInfo }

type FriendsEdge { cursor: String! node: Character }

type PageInfo { hasNextPage: Boolean! endCursor: String }

Query with page info:

{ hero(id: "2001") { friendsConnection(first: 2) { totalCount edges { cursor node { name } } pageInfo { hasNextPage endCursor } } } }

Response:

{ "data": { "hero": { "friendsConnection": { "totalCount": 3, "edges": [ { "cursor": "Y3Vyc29yMQ==", "node": { "name": "Luke Skywalker" } }, { "cursor": "Y3Vyc29yMg==", "node": { "name": "Han Solo" } } ], "pageInfo": { "hasNextPage": true, "endCursor": "Y3Vyc29yMg==" } } } } }

Second page:

{ hero(id: "2001") { friendsConnection(first: 2, after: "Y3Vyc29yMg==") { edges { node { name } } pageInfo { hasNextPage } } } }

Response (if Leia is last):

{ "data": { "hero": { "friendsConnection": { "edges": [ { "node": { "name": "Leia Organa" } } ], "pageInfo": { "hasNextPage": false } } } } }

  • totalCount: Total items (optional).
  • hasNextPage: Signals the end.
  • endCursor: Next page’s starting point.

The Relay Connection Specification

The Relay project formalises this cursor-based pattern. Key rules:

  • Use edges with node and cursor.
  • Include PageInfo with hasNextPage, endCursor, and optionally startCursor.
  • Support first: Int and after: String arguments.

interface Character { friendsConnection(first: Int, after: String): FriendsConnection! }

type FriendsConnection { totalCount: Int edges: [FriendsEdge] pageInfo: PageInfo! }

type FriendsEdge { cursor: String! node: Character }

type PageInfo { hasNextPage: Boolean! endCursor: String }

Wrapping Up: Your Pagination Playbook

Pagination in GraphQL is a journey from simple slices to sophisticated cursor-based connections. Offset is easy but risky, ID-based adds stability, and cursor-based reigns supreme with flexibility and metadata. This playbook equips you to handle any list — whether it’s friends, posts, or galactic allies!

What’s your favourite pagination trick? Drop a comment below — I’d love to hear! Clap 👏, follow for more, and share on LinkedIn to spread the pagination wisdom. 🚀


메타데이터
post_id
8cd8f59d92ee
slug
mastering-graphql-pagination-from-basics-to-advanced-cursor-based-strategies-8cd8f59d92ee
url
https://medium.com/@mariumgoraya13/mastering-graphql-pagination-from-basics-to-advanced-cursor-based-strategies-8cd8f59d92ee
canonical_url
https://medium.com/@mariumgoraya13/mastering-graphql-pagination-from-basics-to-advanced-cursor-based-strategies-8cd8f59d92ee
author_url
https://medium.com/@mariumgoraya13
status
ok
fetched_at
2026-07-18 00:26:52