skip to content
Samuel Edwin's Website

GraphQL - Pagination Best Practices

/ 5 min read

Why implement pagination?

It is very common for applications to fetch a large number of items from the server.

Imagine you’re building Instagram, and you need to fetch the latest posts from your friends who has 4829 posts in their account.

Fetching all the posts at once will slow down the performance of your application.

What Instagram and many other applications do is to use pagination, fetching 20 posts at a time for example.

Types of pagination

There are two types of pagination that we can use in GraphQL:

  1. Offset-based pagination
  2. Cursor-based pagination

Offset based pagination allows the users to jump to a specific page directly.

The problem with offset based pagination is the implementation tends to be slow for large datasets.

This is because the server needs to fetch all the items before the offset to skip.

For cursor based pagination, the client passes a cursor to the server, which indicates where to start fetching the next set of data.

Cursor based pagination is preferred over offset based pagination because of its performance and will be the focus of this post.

Do not reinvent the wheel

The creators of GraphQL at Facebook (now Meta) has provided us with a standard way to implement cursor based pagination, or what many people call Relay style pagination.

It is developed based on their need to do paginations on their feeds.

Is it mandatory to use this standard?

No, but this standard is good enough for most use cases and we don’t need to reinvent the wheel.

Let’s take a look at how it works.

Cursor-based pagination overview

This is how the cursor based pagination looks like in a product search query.

searchProducts(keyword: "phone") {
pageInfo {
hasNextPage
endCursor
}
edges {
cursor
node {
id
name
description
price
}
}
}

Looks like there’s a lot going on, so let’s break it down.

PageInfo

pageInfo {
hasNextPage
endCursor
}

When we use cursor based pagination, we rely on pageInfo to determine if there are more pages to fetch.

This is what the page info response looks like when there are more pages to fetch.

{
"pageInfo": {
"hasNextPage": true,
"endCursor": "YXJyYXljb25uZWN0aW9uOjIw"
}
}

The endCursor will be used to fetch the next set of data.

searchProducts(
keyword: "phone",
after: "YXJyYXljb25uZWN0aW9uOjIw" # previous endCursor
) {
pageInfo {
hasNextPage
endCursor
}
edges {
# ...
}
}

Use the endCursor to fetch the next set of data until there are no more pages to fetch.

{
"pageInfo": {
"hasNextPage": false,
"endCursor": null
}
}

What should I use as the cursor?

The cursor can be anything.

It can be the id of the item, or the cursor from your database.

Use the cursor that is most convenient for you.

Edges and nodes

edges {
cursor
node {
id
name
description
price
}
}

The edges is an array of objects that contains the cursor and the node.

Here’s an example of edges response for the product search query.

{
"edges": [
{
"cursor": "YXJyYXljb25uZWN0aW9uOjE=",
"node": {
"id": "1",
"name": "Product 1",
"description": "Description 1",
"price": 100
}
},
{
"cursor": "YXJyYXljb25uZWN0aW9uOjI=",
"node": {
"id": "2",
"name": "Product 2",
"description": "Description 2",
"price": 200
}
}
]
}

As you can see, the node contains the actual data that we need.

As for the cursor, it’s the same cursor type that is used in the PageInfo.

You can the edge cursor as the pagination identifier for more advanced use cases.

Forward backward pagination and search parameters

Cursor based pagination supports forward and backward pagination.

Any query that supports pagination should have the following parameters:

  1. first: The number of items to fetch
  2. after: The cursor to start fetching the next set of items
  3. last: The number of items to fetch
  4. before: The cursor to start fetching the previous set of items
# forward pagination
searchProducts(
keyword: "phone",
first: 20,
after: "YXJyYXljb25uZWN0aW9uOjIw"
) {
# ...
}
# backward pagination
chatHistory(
roomID: "1",
last: 20,
before: "YXJyYXljb25uZWN0aW9uOjIw"
) {
# ...
}

Forward pagination

Forward pagination is the most common type of pagination and the one that is used in the example above.

In order to paginate forward, the query should support two types of parameters:

  1. first: the number of items to fetch
  2. after: the cursor to start fetching the next set of items
searchProducts(
keyword: "phone",
first: 20,
after: "YXJyYXljb25uZWN0aW9uOjIw"
) {
pageInfo {
hasNextPage
endCursor
}
edges {
# ...
}
}

Or another example is when we want to fetch the next 20 posts from a post.

comments(
postID: "1",
first: 20,
after: "YXJyYXljb25uZWN0aW9uOjIw"
) {
pageInfo {
hasNextPage
endCursor
}
edges {
cursor
node {
id
content
createdAt
}
}
}

Backward pagination

There are some cases where we would want to paginate backwards.

For example, in a chat application, we would want to paginate backwards to fetch the previous messages.

In order to paginate backward, the query should support two types of parameters:

  1. last: the number of items to fetch
  2. before: the cursor to start fetching the previous set of items
searchProducts(
keyword: "phone",
last: 20,
before: "YXJyYXljb25uZWN0aW9uOjIw"
) {
pageInfo {
# instead of hasNextPage and endCursor
# we have hasPreviousPage and startCursor for
# backward pagination
hasPreviousPage
startCursor
}
edges {
cursor
node {
id
name
description
price
}
}
}

Best practices

  • When you know a query will return a large number of items, use pagination by default.
  • When doing forward pagination, only use first and after.
  • When doing forward pagination, only use hasNextPage and endCursor to fetch the next set of data.
  • When doing backward pagination, only use last and before.
  • When doing backward pagination, only use hasPreviousPage and startCursor to fetch the previous set of data.