Bird
Raised Fist0
GraphQLquery~15 mins

Cursor-based pagination in GraphQL - Deep Dive

Choose your learning style10 modes available
Overview - Cursor-based pagination
What is it?
Cursor-based pagination is a way to split large lists of data into smaller parts, called pages, using a unique marker called a cursor. Instead of counting pages by numbers, it uses these cursors to remember where the last page ended. This method helps fetch data efficiently and smoothly, especially when data changes often or is very large. It is commonly used in APIs like GraphQL to load data bit by bit.
Why it matters
Without cursor-based pagination, loading large data sets can be slow and unreliable, especially if data changes between requests. Traditional page-number pagination can skip or repeat items when data updates. Cursor-based pagination solves this by using a stable position marker, making data loading faster and more consistent. This improves user experience in apps and reduces server load.
Where it fits
Before learning cursor-based pagination, you should understand basic pagination concepts and how APIs fetch data. After this, you can explore advanced GraphQL features like Relay connections and how cursor pagination integrates with filtering and sorting.
Mental Model
Core Idea
Cursor-based pagination uses a unique position marker to fetch the next set of data, ensuring smooth and reliable navigation through large or changing lists.
Think of it like...
Imagine reading a book with a bookmark. Instead of remembering the page number, you keep the bookmark where you stopped. When you come back, you start reading exactly from that spot, no matter if pages were added or removed before it.
┌───────────────┐
│ Data List     │
├───────────────┤
│ Item 1        │
│ Item 2        │
│ Item 3        │ ← Cursor here
│ Item 4        │
│ Item 5        │
└───────────────┘

Fetch next page using cursor pointing at Item 3 to get Item 4 and Item 5.
Build-Up - 6 Steps
1
FoundationUnderstanding basic pagination
🤔
Concept: Learn what pagination is and why we split data into pages.
Pagination means dividing a large list of items into smaller chunks or pages. For example, a website might show 10 products per page instead of all at once. This helps users load and view data faster and easier.
Result
You understand why we don't load all data at once and what a page means in data fetching.
Knowing why we paginate helps you appreciate the need for efficient ways to handle big data.
2
FoundationIntroduction to cursors
🤔
Concept: A cursor is a unique marker that points to a specific item in a list.
Instead of using page numbers, cursor pagination uses a cursor to mark the last item seen. This cursor is often a unique ID or encoded string representing the position in the list.
Result
You can identify what a cursor is and how it marks a position in data.
Understanding cursors is key to grasping how cursor-based pagination tracks progress.
3
IntermediateHow cursor pagination works in GraphQL
🤔Before reading on: do you think cursor pagination uses page numbers or unique markers? Commit to your answer.
Concept: Cursor pagination fetches data after a given cursor, not by page number.
In GraphQL, cursor pagination uses arguments like 'after' with a cursor value to fetch the next set of items. The server returns edges with nodes and cursors, allowing clients to request the next page starting after the last cursor.
Result
You see how queries use 'after' cursors and how responses include cursors for the next page.
Knowing that cursor pagination relies on unique markers rather than page numbers helps avoid common bugs with changing data.
4
IntermediateBenefits over offset pagination
🤔Before reading on: do you think offset pagination or cursor pagination handles data changes better? Commit to your answer.
Concept: Cursor pagination avoids problems caused by data changes during paging.
Offset pagination uses page numbers and skips, which can cause missing or repeated items if data changes. Cursor pagination uses stable cursors, so even if data is added or removed, the next page starts exactly where the last ended.
Result
You understand why cursor pagination is more reliable for dynamic data.
Recognizing the limitations of offset pagination explains why cursor pagination is preferred in many real-world apps.
5
AdvancedImplementing Relay-style connections
🤔Before reading on: do you think Relay connections include total counts or just cursors? Commit to your answer.
Concept: Relay connections standardize cursor pagination with edges, nodes, and pageInfo.
Relay-style pagination returns a connection object with edges (each with a node and cursor) and pageInfo (hasNextPage, endCursor). This structure helps clients navigate pages and know if more data exists.
Result
You can read and write GraphQL queries using Relay connections for cursor pagination.
Understanding Relay connections prepares you for advanced GraphQL pagination patterns used in production.
6
ExpertHandling cursor encoding and security
🤔Before reading on: do you think cursors are plain IDs or encoded strings? Commit to your answer.
Concept: Cursors are often encoded to hide internal details and prevent tampering.
To avoid exposing database IDs or internal structure, cursors are encoded (e.g., base64). This encoding also helps keep cursors opaque and secure. Servers decode cursors to find the correct position when fetching data.
Result
You understand why cursors are encoded and how to implement encoding/decoding.
Knowing cursor encoding prevents security leaks and ensures stable pagination even if internal data changes.
Under the Hood
Cursor-based pagination works by storing a unique identifier (cursor) for each item, often based on a stable column like a timestamp or ID. When a client requests the next page, it sends the last cursor received. The server decodes this cursor to find the exact position in the data and fetches the next set of items after it. This avoids counting or skipping rows, which can be slow or inconsistent with changing data.
Why designed this way?
Traditional offset pagination was simple but caused problems when data changed between requests, leading to missing or duplicated items. Cursor pagination was designed to provide a stable, efficient way to page through data without relying on counting or offsets. Encoding cursors hides internal details and prevents clients from manipulating queries incorrectly.
Client Request ──▶ Server
  │                     │
  │  sends cursor       │
  │────────────────────▶│
  │                     │
  │          fetch data after cursor
  │                     │
  │◀────────────────────│
  │  returns data + new cursor
  ▼                     ▼
Myth Busters - 4 Common Misconceptions
Quick: Does cursor pagination always return the same number of items per page? Commit yes or no.
Common Belief:Cursor pagination always returns a fixed number of items per page, just like page numbers.
Tap to reveal reality
Reality:Cursor pagination returns up to the requested number of items, but the actual count can vary due to filtering or data changes.
Why it matters:Assuming fixed page sizes can cause UI bugs or incorrect assumptions about data completeness.
Quick: Is the cursor just a simple row number? Commit yes or no.
Common Belief:A cursor is just a row number or offset in the list.
Tap to reveal reality
Reality:A cursor is a unique identifier or encoded value representing a position, not a simple number.
Why it matters:Treating cursors as offsets can break pagination when data changes or when IDs are not sequential.
Quick: Can you use cursor pagination without encoding cursors? Commit yes or no.
Common Belief:Cursors can be plain IDs without encoding, and it won't cause issues.
Tap to reveal reality
Reality:Using plain IDs exposes internal data structure and can allow clients to manipulate queries, causing security or stability problems.
Why it matters:Not encoding cursors risks data leaks and unstable pagination behavior.
Quick: Does cursor pagination always perform better than offset pagination? Commit yes or no.
Common Belief:Cursor pagination is always faster and better than offset pagination.
Tap to reveal reality
Reality:Cursor pagination is better for large or changing data, but for small static datasets, offset pagination can be simpler and sufficient.
Why it matters:Choosing cursor pagination unnecessarily can add complexity without benefits.
Expert Zone
1
Cursors must be based on stable, unique columns to avoid skipping or repeating items when data changes.
2
Combining cursor pagination with sorting requires careful cursor design to reflect the sort order.
3
Relay-style connections include pageInfo fields that help clients understand pagination state beyond just cursors.
When NOT to use
Avoid cursor pagination for small datasets or when users need direct access to arbitrary pages by number. Use offset pagination or keyset pagination alternatives when sorting is complex or cursors cannot be stable.
Production Patterns
In production GraphQL APIs, cursor pagination is often implemented with Relay connections, encoding cursors as base64 strings of unique IDs or timestamps. Servers optimize queries using indexes on cursor columns. Clients use pageInfo to show loading spinners or 'load more' buttons.
Connections
Keyset pagination
Cursor-based pagination is a form of keyset pagination using unique keys as cursors.
Understanding keyset pagination helps grasp why cursor pagination is efficient and stable compared to offset pagination.
Stateful bookmarks in user interfaces
Both use a saved position to resume progress later.
Knowing how bookmarks work in reading apps clarifies how cursors mark positions in data streams.
Linked lists in computer science
Cursor pagination resembles traversing a linked list node by node using pointers.
Seeing cursors as pointers helps understand why jumping directly to a page number is harder than moving stepwise.
Common Pitfalls
#1Using page numbers with cursor pagination arguments.
Wrong approach:query { items(page: 2, first: 10) { edges { node } } }
Correct approach:query { items(after: "cursor123", first: 10) { edges { node cursor } pageInfo { hasNextPage endCursor } } }
Root cause:Confusing cursor pagination with offset pagination and trying to use page numbers instead of cursors.
#2Exposing raw database IDs as cursors.
Wrong approach:cursor: "123"
Correct approach:cursor: "YXJ0aWNsZV8xMjM=" // base64 encoded string
Root cause:Not encoding cursors leads to security risks and unstable pagination.
#3Using non-unique or unstable fields as cursors.
Wrong approach:cursor based on 'created_at' timestamp without uniqueness
Correct approach:cursor based on 'created_at' + unique ID combined and encoded
Root cause:Using non-unique cursors causes duplicate or missing items when data changes.
Key Takeaways
Cursor-based pagination uses unique position markers called cursors to fetch data pages reliably.
It solves problems of missing or repeated items common in offset pagination when data changes.
Cursors are often encoded to hide internal details and ensure security.
Relay-style connections standardize cursor pagination in GraphQL with edges and pageInfo.
Choosing the right pagination method depends on data size, stability, and user needs.

Practice

(1/5)
1. What is the main purpose of cursor-based pagination in GraphQL?
easy
A. To fetch all data at once without limits
B. To sort data alphabetically
C. To fetch data in small parts using a cursor to mark position
D. To delete data from the database

Solution

  1. Step 1: Understand pagination purpose

    Pagination is used to split large data into smaller parts for easier handling.
  2. Step 2: Identify cursor role

    Cursor marks the position in the data to fetch the next part without overlap or missing items.
  3. Final Answer:

    To fetch data in small parts using a cursor to mark position -> Option C
  4. Quick Check:

    Cursor-based pagination = fetch data in parts [OK]
Hint: Cursor marks position to fetch next data chunk [OK]
Common Mistakes:
  • Thinking pagination fetches all data at once
  • Confusing cursor with sorting
  • Assuming pagination deletes data
2. Which of the following is the correct way to request the first 5 items after a cursor in GraphQL?
easy
A. { items(fetch: 5, cursor: "cursor123") { edges { node } } }
B. { items(limit: 5, startAfter: "cursor123") { edges { node } } }
C. { items(take: 5, afterCursor: "cursor123") { edges { node } } }
D. { items(first: 5, after: "cursor123") { edges { node } } }

Solution

  1. Step 1: Recall GraphQL cursor pagination syntax

    The standard uses first to limit items and after to specify the cursor.
  2. Step 2: Match options with correct syntax

    { items(first: 5, after: "cursor123") { edges { node } } } uses first and after correctly; others use incorrect argument names.
  3. Final Answer:

    { items(first: 5, after: "cursor123") { edges { node } } } -> Option D
  4. Quick Check:

    Use first and after arguments [OK]
Hint: Use first and after arguments for cursor pagination [OK]
Common Mistakes:
  • Using limit instead of first
  • Using startAfter or afterCursor which are invalid
  • Confusing argument names
3. Given this GraphQL query:
{ users(first: 3, after: "YXJyYXljb25uZWN0aW9uOjE=") { edges { node { id name } } pageInfo { hasNextPage endCursor } } }
What does the endCursor represent in the result?
medium
A. The cursor of the last item in the current page
B. The total number of items in the database
C. The cursor of the first item in the current page
D. The number of pages available

Solution

  1. Step 1: Understand pageInfo fields

    endCursor marks the cursor of the last item returned in the current page.
  2. Step 2: Differentiate from other fields

    hasNextPage shows if more pages exist; endCursor is not a count or first item cursor.
  3. Final Answer:

    The cursor of the last item in the current page -> Option A
  4. Quick Check:

    endCursor = last item cursor [OK]
Hint: endCursor points to last item fetched [OK]
Common Mistakes:
  • Confusing endCursor with total count
  • Thinking endCursor is first item cursor
  • Assuming endCursor is page count
4. You wrote this GraphQL query for cursor pagination:
{ posts(first: 10, afterCursor: "abc123") { edges { node { title } } } }
Why does this query fail?
medium
A. The argument should be named after, not afterCursor
B. The first argument must be a string, not a number
C. The edges field is invalid in cursor pagination
D. The cursor value cannot be a string

Solution

  1. Step 1: Check argument names for cursor pagination

    The correct argument to specify the cursor is after, not afterCursor.
  2. Step 2: Validate other parts

    first accepts a number; edges is valid; cursor values are strings.
  3. Final Answer:

    The argument should be named after, not afterCursor -> Option A
  4. Quick Check:

    Use 'after' argument name [OK]
Hint: Use 'after' not 'afterCursor' for cursor argument [OK]
Common Mistakes:
  • Using incorrect argument names
  • Thinking first must be string
  • Misunderstanding edges field usage
5. You want to fetch all comments in batches of 4 using cursor-based pagination. The first query is:
{ comments(first: 4) { edges { node { id text } } pageInfo { hasNextPage endCursor } } }
To fetch the next batch, which query should you use?
hard
A. { comments(limit: 4, after: "endCursor_value") { edges { node { id text } } } }
B. { comments(first: 4, after: "endCursor_value") { edges { node { id text } } pageInfo { hasNextPage endCursor } } }
C. { comments(last: 4, after: "endCursor_value") { edges { node { id text } } } }
D. { comments(first: 4, before: "endCursor_value") { edges { node { id text } } } }

Solution

  1. Step 1: Understand how to continue pagination

    Use the after argument with the endCursor value from the previous page to fetch the next batch.
  2. Step 2: Check argument correctness

    first: 4 limits to 4 items; after moves cursor forward; before or last are for backward pagination.
  3. Final Answer:

    { comments(first: 4, after: "endCursor_value") { edges { node { id text } } pageInfo { hasNextPage endCursor } } } -> Option B
  4. Quick Check:

    Use first and after with endCursor for next batch [OK]
Hint: Use first and after with endCursor to paginate forward [OK]
Common Mistakes:
  • Using before instead of after for next page
  • Using last instead of first for forward pagination
  • Using limit instead of first argument
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard