Software Artist’s blog

Keep it simple!

Pagination With GraphQL in Rails

We generally do not prefer to retrieve all the required data from the database in one go.

Instead, we want it to be loaded page by page for performance and user experience.

With REST API in Rails, we pass page and per_page arguments in the URL and get the paginated data when using pagination library like mislav/will_paginate. But for GraphQL, we need to configure the backend for paginating the data.

In this blog, we will discuss how can we implement pagination with GraphQL in a Rails project.

Suppose we have a Rails application with GraphQL APIs implemented using graphql gem.

In the application, we have a Types::QueryType class where items query is implemented to fetch the items list like below.

module Types
  class QueryType < Types::BaseObject
    # Add `node(id: ID!) and `nodes(ids: [ID!]!)``
    include GraphQL::Types::Relay::HasNodeField
    include GraphQL::Types::Relay::HasNodesField

    # Add root-level fields here.
    # They will be entry points for queries on your schema.

    field :items, [Types::ItemType],null: false

    def items
      Item.all
    end
  end
end

Whenever we make a GraphQL query for fetching items like this.

{
  items {
    title
    description
  }
}

It returns all the items present in the database like this.

{
  "data": {
    "items": [
      {
        "title": "Martian Chronicles",
        "description": "Cult book by Ray Bradbury"
      },
      {
        "title": "The Martian",
        "description": "Novel by Andy Weir about an astronaut stranded on Mars trying to survive"
      },
      {
        "title": "Doom",
        "description": "A group of Marines is sent to the red planet via an ancient Martian portal called the Ark to deal with an outbreak of a mutagenic virus"
      },
      {
        "title": "Mars Attacks!",
        "description": "Earth is invaded by Martians with unbeatable weapons and a cruel sense of humor"
      },
      {
        "title": "Reem",
        "description": "Test A group of Marines is sent to the red planet via an ancient Martian portal called the Ark to deal with an outbreak of a mutagenic virus"
      },
      {
        "title": "Possible Mars Attacks!",
        "description": "Earth is invaded by Martians with unbeatable weapons and a cruel sense of humor"
      }
    ]
  }
}

Now let’s see how can we implement the changes to fetch the items in a pagination manner using GraphQL query.

First of all, let’s add the gem graphql-pagination in our application using the following command.

$ bundle add graphql-pagination

This command will add the gem graphql-pagination to the application Gemfile as well as it will run bundle install to install the gem.

Once the gem is installed, we are ready to update the above mentioned Types::QueryType class for implementing pagination for items.

Let’s update the field :items to use Types::ItemType.collection_type instead of [Types::ItemType] and expect the arguments page and limit in the query.

Here page means the page number in the list and limit means the no. of records per page. We can set a default limit to some value, here we will set it as 2 for a test.

With these changes the field :items implementation will look like this.

field :items, Types::ItemType.collection_type, null: false do
  argument :page, Integer, required: false
  argument :limit, Integer, required: false, default_value: 2
end

In addition to the above changes, we will also need to update the items method for accepting the page and limit arguments and paginating the records.

We are already having mislav/will_paginate gem in the application so we will use it for the pagination.

Now the items method will look like this.

def items(limit:, page: nil)
  Item.paginate(page: page, per_page: limit)
end

After all these changes the Types::QueryType class will look like this.

module Types
  class QueryType < Types::BaseObject
    # Add `node(id: ID!) and `nodes(ids: [ID!]!)`
    include GraphQL::Types::Relay::HasNodeField
    include GraphQL::Types::Relay::HasNodesField

    # Add root-level fields here.
    # They will be entry points for queries on your schema.

    field :items, Types::ItemType.collection_type, null: false do
      argument :page, Integer, required: false
      argument :limit, Integer, required: false, default_value: 2
    end

    def items(limit:, page: nil)
      Item.paginate(page: page, per_page: limit)
    end
  end
end

Now, we are implementation ready for fetching the items in the pagination manner

We need to remember that, the items fields data will now always be fetched inside the collection object and we will need to mention the items related fields inside the collection.

We can modify the previous GraphQL query to fetch the items in the collection like this.

{
  items {
    collection {
      title
      description
    }
  }
}

It will return the items collection with the default limit of 2.

{
  "data": {
    "items": {
      "collection": [
        {
          "title": "Martian Chronicles",
          "description": "Cult book by Ray Bradbury"
        },
        {
          "title": "The Martian",
          "description": "Novel by Andy Weir about an astronaut stranded on Mars trying to survive"
        }
      ]
    }
  }
}

Let’s try by passing the page and limit values.

{
  items(limit: 3, page: 2) {
    collection {
      title
      description
    }
  }
}

The data returned is.

{
  "data": {
    "items": {
      "collection": [
        {
          "title": "Mars Attacks!",
          "description": "Earth is invaded by Martians with unbeatable weapons and a cruel sense of humor"
        },
        {
          "title": "Reem",
          "description": "Test A group of Marines is sent to the red planet via an ancient Martian portal called the Ark to deal with an outbreak of a mutagenic virus"
        },
        {
          "title": "Possible Mars Attacks!",
          "description": "Earth is invaded by Martians with unbeatable weapons and a cruel sense of humor"
        }
      ]
    }
  }
}

With graphql-pagination gem, we can also request the following pagination metadata in the query alongside collection.

metadata {
  totalPages
  totalCount
  currentPage
  limitValue
}

Note: The graphql-pagination gem doesn’t support totalCount with will_paginate gem because will_paginate gem has method total_entries instead of total_count for retreiving the total no. of entries. In this gist, we can see how to override Graphql::Pagination::MetadataType class for supporting total_count with will_paginate.

Let’s request the metadata for items.

{
  items(limit: 3, page: 2) {
    collection {
      title
      description
    }
    metadata {
      totalPages
      totalCount
      currentPage
      limitValue
    }
  }
}

The result would be like this

{
  "data": {
    "items": {
      "collection": [
        {
          "title": "Mars Attacks!",
          "description": "Earth is invaded by Martians with unbeatable weapons and a cruel sense of humor"
        },
        {
          "title": "Reem",
          "description": "Test A group of Marines is sent to the red planet via an ancient Martian portal called the Ark to deal with an outbreak of a mutagenic virus"
        },
        {
          "title": "Possible Mars Attacks!",
          "description": "Earth is invaded by Martians with unbeatable weapons and a cruel sense of humor"
        }
      ],
      "metadata": {
        "totalPages": 2,
        "totalCount": 6,
        "currentPage": 2,
        "limitValue": 3
      }
    }
  }
}

In this way, we can implement pagination for other query types. But remember that the queries now need to be modified to use collection for fetching the list once modified for the pagination.

The graphql-pagination gem works with kaminari pagination library without overriding metadata unlike will_paginate gem.