Hi there!
There are plenty of ways to handle pagination in Ruby apps and some of them are very popular. There is a chance you've heard about the pagy
gem, kaminari
, jsom-pagination
or other gems allowing you to easily work with paginated resources in web applications.
All that is great, but I'm not sure if you know, that ROM supports Pagination out of the box. And so Hanami does.
In this episode, I'll show you how to quickly implement pagination from scratch for your Hanami projects.
Let's begin.
Starting point.
I have this little application here allowing me to list articles in my browser.
# /app/actions/articles/index.rb
module Sandbox
module Actions
module Articles
class Index < Get
include Deps[repo: 'repositories.articles']
def handle(req, res)
collection = repo.articles_with_authors
res.body = serialize(collection.to_a)
end
end
end
end
end
It just fetches the articles from the repositories and applies the serialization to the resulting collection.
The serialization logic is extracted into the GET
action class which my article's index inherits from.
# frozen_string_literal: true
require 'byebug'
module Sandbox
module Actions
# Action aggregating common logic for serving
# GET HTTP requests
#
class Get < Action
after :success
private
def serialize(model)
# ...serialization logic
end
def success(req, res)
res.status = 200
end
end
end
end
I use the after
callback to set my HTTP response status to:ok
after the action successfully processes the request.
important
If you want to know more about setting this persistence layer up, check out my episodes related to ROM, I’ve already covered several of them. Or, check out the Hanami Mastery PRO, where I’m trying to share more advanced tutorials in a regular manner.
All seems to be fine, however, in the case of hundreds of articles in my database, I definitely would not like to fetch all of them and render a return to the response at once.
Instead, I'd like to paginate the result, to always fetch a little subset of data, allowing my client to control how many items they want to process.
Setting up ROM pagination plugin in Hanami
ROM has built-in support for the pagination via the plugin system, and we'll going to leverage this to speed things up.
In the persistence provider, I'm going to enable the pagination
plugin for sql
component, for relations
.
# config/providers/persistence.rb
config.plugin(:sql, relations: :pagination)
Now, when I fetch any relation from the database, I can suddenly access the page
and per_page
methods, allowing me to control how many records are fetched from the db.
repo = container['repositories.articles']
repo.articles.count
# => 2
repo.articles.per_page(1).page(2).to_a
# => []
With this, I can quickly create my pagination feature.
Implement Pagination module for action.
I'm going to create a utility folder with a paginable
module in it, that I'll include in my action later.
it will have a single method named paginate
, that accepts the relation and the params hash. Inside I'm going to paginate the relation exactly as I've just shown you in the terminal.
# frozen_string_literal: true
module Sandbox
module Utils
module Paginable
def paginate(relation, params)
relation.
per_page(params[:per_page]).
page(params[:page])
end
end
end
end
Working with relations
For the sake of this example, my repository return relation object, that I can call pagination methods on. However, it's not the encouraged practice.
Repositories should return already finalized collection - an array and should be used for simple queries. For any more advanced usage, like complicated queries, dynamic filters, or pagination, I encourage using Query Objects.
Now, in the articles index action, I just need to include the newly created module, and add the pagination step to the handle
method.
# app/actions/articles/index.rb
include Utils::Paginable
def handle(req, res)
collection = repo.articles_with_authors
paginated = paginate(collection, req.params)
res.body = serialize(paginated)
end
With this, I can already paginate my resources by visiting the browser.
With this, I can already paginate my resources by visiting the localhost:2300/articles?page=2&per_page=1
Paginated resources response
Validating pagination parameters.
This basic setup is working, however, it's not error-prone.
I don't setup the default values for my pagination parameters, which will break my fetching method. Also, I do not validate the input parameters, so I don't secure my app from unpredictable input, like extremely large, or negative numbers.
This is why I recommend validating your GET requests, similar to what you do for creating or updating resources.
Here I'm going to set the default constants and define the validation schema. I'm going to set the page
parameter to optional, but in case it's passed in, it needs to be filled in.
The expected type would be the Integer, defaulting to 1. I'll also make sure, that the page is always greater than 1. I guess it should be a constant too, but... whatever :).
Then I'm going to repeat that for per_page
parameter, with the difference, that this will need to be also smaller than 50
# lib/sandbox/utils/paginable.rb
module Sandbox
module Utils
module Paginable
DEFAULT_PAGE = 1
DEFAULT_SIZE = 10
PaginationSchema = Dry::Schema.Params do
optional(:page).filled(Types::Coercible::Integer.default(DEFAULT_PAGE), gteq?: 1)
optional(:per_page).filled(Types::Coercible::Integer.default(DEFAULT_SIZE), gteq?: 1, lteq?: 50)
end
# ...
end
end
end
Now let me use it. I'll extract the validation to a seaparate method and call it using the params hash as an input.
If the validation succeeds, I’ll just return from the method, but otherwise, I’m going to halt the processing and immediately return the error information to the browser, setting up the HTTP code to 400.
def paginate(relation, params)
validate_pagination(params.to_h)
relation.
per_page(result[:per_page]).
page(result[:page])
end
def validate_pagination(params)
result = PaginationSchema.call(params)
return if result.success?
halt 400, result.errors.to_h.to_json
end
Now when I call my browser with invalid pagination parameters, I'll end up with the more meaningful error message.
Paginated resources response
Awesome!
Summary
Pagination is a simple feature, but even such a little thing has its caveats. I hope that with this short episode I've shown you why it's useful to validate all input incoming to your system and how to paginate resources using Hanami and ROM in your web applications.
Thanks
I want to especially thank my recent sponsors,
for supporting this project, I really appreciate it!
Add your suggestion to our discussion panel!
I'll gladly cover them in the future episodes! Thank you!