ActiveRecord to JSON API Part 2: Solving Pagination
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:
- json_api_client for reading data from the API
- jsonapi-resources for creating the internal JSON API endpoints
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.
Link Params
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!