Heading image for post: ActiveRecord to JSON API Part 2: Solving Pagination

ActiveRecord to JSON API Part 2: Solving Pagination

Profile picture of Mary Lee

While building an internal JSON API for a Rails application, we came across a few issues with pagination.

  • Making Kaminari work
  • Resolving problems with pagination link parameters
  • Enabling pagination while still allowing users to get everything

Background

We were building an internal JSON API using the following libraries:

For more information on our goals and set up for the internal API, see part 1 of this series.

Kaminari

The application we were working on was using Kaminari for pagination, and one of the first things we noticed during our switch from ActiveRecord to JsonApiClient::Resources was that pagination no longer worked, but only when the page number param was not present.

Upon investigating the source code for JsonApiClient, we noticed that when the page number param was blank during pagination, the page was resolved to 0, not 1. Kaminari couldn’t handle the page number being 0, so we needed to figure out how to change the JsonApiClient behavior to have the page number default to 1, rather than 0.

As it turns out, the JsonApiClient library is incredibly easy to configure and override, using class attributes for managing the objects used to perform different functions for the resources, including pagination. This meant that we could create a custom paginator, inheriting from the default paginator in JsonApiClient, and override its current_page method to handle the potential missing page parameter the way we needed it to.

class CustomPaginator < JsonApiClient::Paginating::Paginator
  def current_page
    (params[page_param] || 1).to_i
  end
end

Since we had set up all of our JsonApiClient::Resources to inherit from one BaseResource, we updated that resource to use our new custom paginator.

class BaseResource < JsonApiClient::Resource
  # …
  self.paginator = CustomPaginator
  # … 
end

With that, Kaminari was working again.

Our next barrier came from link parameters for pagination.

The jsonapi-resources library returns links with page params url encoded, like page%5Bnumber%5D=1&page%5Bsize%5D=25. Those params were getting parsed incorrectly by the JsonApiClient, resulting in pagination params that looked like { "page[number]" => 1, "page[size]" => 25 }, which the JsonApiClient library wasn’t expecting, and so didn’t use for pagination.

From the documentation, we saw that we could tell the library what it should be looking for when it came to pagination params, so we created an initializer to set that data.

# config/initializers/json_api_client.rb
JsonApiClient::Paginating::Paginator.page_param = "page[number]"
JsonApiClient::Paginating::Paginator.per_page_param = "page[size]"

This worked in a sense that the JsonApiClient was now getting and using the page params properly, but when requesting additional pages from the API, it was sending nested parameters, like “page[page[number]]”, breaking pagination on the JSONAPI::Resource side.

We decided that since we were already overriding the default JsonApiClient pagination for the sake of Kaminari, we may as well do it again to get our params working appropriately. We removed the initializer we created to set the page parameters, and overrode the params_for_uri method with our CustomPaginator.

class CustomPaginator < JsonApiClient::Paginating::Paginator
  # … 

  def params_for_uri(uri)
    return {} unless uri
    query = Addressable::URI.parse(uri).query
    Rack::Utils.parse_nested_query(query)["page"] || {}
  end
end

With those changes, our pagination was finally fully operational.

Getting Everything

Our final struggle with pagination was dealing with allowing users to fetch everything. We wanted to have our API return all resources when pagination params were not present. This was a problem since the JSONAPI::Resource default pagination, once enabled, was not able to be turned off.

Our solution to this was to create a new paginator for the JSONAPI::Resources, inheriting from the default paginator, and overriding methods as we needed.

class LocalAPI::CustomPaginator < PagedPaginator
  attr_reader :params

  def initialize(params)
    return unless params
    @params = params
    super
  end

  def apply(relation, _order_options)
    return relation unless params
    super
  end

  def links_page_params(options = {})
    return {} unless params
    super
  end
end

With the custom paginator created, we had to make sure that the JSONAPI::Resources were using it. This was accomplished by adding an initializer, and specifying our new paginator as the default.

# config/initializers/jsonapi_resources.rb
JSONAPI.configure do |config|
  # This gets resolved to LocalAPI::CustomPaginator
  config.default_paginator = "LocalAPI::Custom"
end

Now, we could get all of our resources for the API, as long as no pagination params were present.

Closing Thoughts

Both of the libraries we used were extremely customizable, which made making changes mostly painless, especially as we came across further problems getting our API to work as we wanted.

With pagination, the biggest battle was figuring out what exactly we needed to override in the default paginator classes for both libraries. Once we identified the methods that were causing us problems, we were able to resolve the issues pretty quickly.

Check out the rest of this series!

More posts about rails json api