Screenshot of a barebones HTML table with pagination links underneath

Integrating Pagy with Hanami (2025 edition)

Back in 2018 I wrote a post about connecting Hanami (then 1.x) and Pagy gem together. My verdict was not that favorable. I had to write quite a lot of glue code to make it work and perhaps the worst thing was that I had to pass the request object to the template to make it work.

However, things have changed since then:

  1. Hanami is now 2.3, its persistence layer is more mature, based on ROM and allows to fall back to almost-bare-Sequel via relations.
  2. Pagy released version 43 this week. It’s advertised as a complete rewrite of its internals and APIs.

We needed a leap version to unequivocally signaling that it’s not just a major version: it’s a complete redesign of the legacy code at all levels, usage and API included.

There’s no better time to take it for another spin then!

The code

I quickly spun up a fresh Hanami app, created a migration:

ROM::SQL.migration do
  change do
    create_table :people do
      primary_key :id
      column :name, String, null: false
      column :email, String, null: false
      column :birth_date, Date, null: false
      column :added_at, DateTime, null: false
    end
  end
end

Then created a relation:

module Pagynation
  module Relations
    class People < Hanami::DB::Relation
      schema :people, infer: true
    end
  end
end

Next I generated the action and added Pagy to it.

require "pagy"

module Pagynation
  module Actions
    module People
      class List < Pagynation::Action
        include Deps["relations.people"]
        include Pagy::Method

        def handle(request, response)
          pagy, records = pagy(people.order(:name), request:, client_max_limit: 40)
          response.render view, pagy: pagy, people: records
        end
      end
    end
  end
end

This looks more or less like the tutorial code from the Pagy website. The only exception is that I have to pass the request directy. If not passed, Pagy expects self.request to be defined. This does not work with Hanami, where the request is passed as an argument (in a functional flavour), not burned into the controller instance.

Let’s now examine the view:

module Pagynation
  module Views
    module People
      class List < Pagynation::View
        expose :people, decorate: false

        expose :paginator, decorate: false do |pagy:|
          pagy
        end
      end
    end
  end
end

This is standard Hanami stuff. We passed :pagy and :people from the action via the render method, now we need to pass it on to the template. I used decorate: false here to keep the things as simple as possible (data is a simple hash, not objects). With people and paginator exposed, all that’s left is to craft a template using it.

<h1>People list</h1>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Email</th>
      <th>Date of Birth</th>
    </tr>
  </thead>
  <tbody>
    <% people.each do |person| %>
      <tr>
        <td><%= person[:name] %></td>
        <td><%= person[:email] %></td>
        <td><%= person[:birth_date] %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<div>
  <%= raw paginator.info_tag %>
</div>

<div>
  <%= raw paginator.series_nav %>
</div>

The paginator.info_tag and paginator.series_nav are Pagy’s HTML helper to render some status info and pagination links. They are, of course, quite barebones, but more than enough to test stuff.

This all led me to this beauty, with working limit URL params, working links etc.

Screenshot of a barebones HTML table with pagination links underneath

New verdict

This is basically great.

Everything worked pretty much out-of-the-box. If I were to imagine how seamless a pagination library for Hanami could be, this would likely look like that. Great improvements from Pagy maintainer.

The repository to try it out yourself is available here on Codeberg.

Happy paginating!

Similar Posts