Ruby has no shortage of pagination gems, yet none of them handle JSON:API pagination cleanly out of the box. That is the problem JSOM:Pagination solves.

What is wrong with existing Ruby pagination gems?

The most popular options:

Kaminari and Will Paginate are tightly coupled to ActiveRecord. That makes them easy to set up if you are inside a standard Rails model, but creates two significant problems:

  • Hard to use outside ActiveRecord collections. If you paginate a plain array, an Elasticsearch result, or an API response, you fight the abstraction.
  • Slow. Both gems carry overhead from their deep ActiveRecord integration.

Pagy solves both issues. It is dozens of times faster and genuinely framework-agnostic, working in any Rack application.

Where does Pagy fall short?

Pagy has two issues that matter for JSON:API applications.

Architectural assumptions that break outside Rails controllers

Pagy relies on modules and assumptions that force extensive configuration when used outside standard Rails controllers. For example:

  • Pagy::Backend assumes a params method exists in the including class
  • Adding metadata and related links requires an extension that assumes a request method is accessible

None of this is a problem inside Rails controllers. But if you want pagination in a service object, a background job, or a non-Rails Rack app, you end up fighting the gem’s assumptions.

No built-in JSON:API support

JSON:API specifies pagination via nested query parameters: page[size] and page[number]. Because Pagy is framework-agnostic, it does not handle this format natively. You always end up writing adapter code.

How does JSOM:Pagination solve this?

JSOM:Pagination is a thin wrapper around Pagy that provides:

  • A standalone class usable anywhere in your application, not just controllers
  • Built-in JSON:API parameter parsing for page[size] and page[number]
  • Automatic meta and link generation compliant with the JSON:API spec

Usage is straightforward. Instantiate a paginator:

require 'jsom-pagination'
paginator = JSOM::Pagination::Paginator.new

Paginate any collection by passing it with parameters and a base URL:

collection = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
pagination_params = { number: 2, size: 3 }
paginated = paginator.call(collection, params: pagination_params, base_url: 'https://example.com')

What do you get back?

Injecting dependencies into the paginator means it works anywhere: controllers, service objects, background jobs, non-Rails apps.

All meta information is accessible immediately:

paginated.meta
# => #<JSOM::Pagination::MetaData total=10 pages=4>

The same applies to links for related pages, generated automatically with the correct JSON:API format.

Summary

Pagination should be a solved problem. In the Ruby ecosystem, it is surprisingly not. Kaminari and Will Paginate are coupled to ActiveRecord. Pagy is fast but assumes Rails controllers. JSOM-Pagination fills the gap for anyone building JSON:API applications who needs pagination that works everywhere without fighting framework assumptions.

Practical Implementation: The USEO Approach

We built JSOM-Pagination out of a real need on the Yousty HR portal. The application serves a JSON:API to multiple front-end clients, and pagination logic lived in three different places: a Rails controller concern, a service object with manual Pagy configuration, and a GraphQL resolver with its own pagination wrapper. Each one parsed page[size] and page[number] differently, and link generation was inconsistent across endpoints.

After extracting the common pattern into JSOM-Pagination, all three call sites collapsed into a single Paginator.new.call(collection, params:, base_url:) invocation. The service object and GraphQL resolver no longer needed to fake params or request methods to satisfy Pagy’s assumptions.

On Triptrade, we used the same gem to paginate search results from an external availability API. The results were plain Ruby arrays (no ActiveRecord), so Kaminari and Will Paginate were off the table entirely. JSOM-Pagination handled it without any special configuration, and the response links matched the JSON:API spec that the React front-end expected.