Skip to main content

Pagination Gem vs .limit()

When an API returns a list, you have two choices: cap the results with .limit(), or add a pagination gem. They solve different problems.

When .limit() Is Enough

If you just need to prevent a query from returning thousands of rows, .limit() does the job:

Message.order(created_at: :desc).limit(20)

This works fine when the frontend only needs to display a fixed number of records, like a "recent activity" feed, a dashboard widget.

The limitation: .limit() has no concept of pages, total count, or what comes next.

When You Need a Pagination Gem

Once the frontend needs to render page controls or navigate between pages, you need metadata alongside the data:

{
"data": [...],
"pagination": {
"page": 2,
"pages": 12,
"count": 235,
"next": 3,
"prev": 1
}
}

Pagination gem handles the LIMIT/OFFSET math and exposes these values for you.

Choosing a Gem

Three gems come up most often in Rails projects:

will_paginate was the original standard and is still found in many existing codebases. The API is straightforward, but the gem is no longer actively maintained.

Kaminari is widely used and integrates cleanly with ActiveRecord scopes. Most Rails projects you encounter will likely be using this.

Pagy is the modern recommendation for new projects. It is significantly faster and uses far less memory than both alternatives. For a JSON API, this is the clear choice.

Setting Up Pagy for an API

Install

# Gemfile
gem "pagy"
bundle install

Controller

Include Pagy::Method and use pagy(:offset, ...) for standard offset-based pagination:

class MessagesController < ApplicationController
include Pagy::Method

def index
@pagy, @messages = pagy(:offset, Message.order(created_at: :desc), limit: 20)

render json: {
data: @messages,
pagination: @pagy.data_hash
}
end
end

pagy returns two objects: the pagination metadata and the scoped collection. data_hash gives you a plain hash ready to serialize into the response.

The output looks like:

{
"count": 235,
"page": 2,
"limit": 20,
"pages": 12,
"prev": 1,
"next": 3
}

Capping per_page to Prevent Abuse

If you expose limit as a query param, always enforce a maximum. Without a cap, a client could request ?limit=10000 and hit your database hard.

def index
limit = params.fetch(:limit, 20).to_i.clamp(1, 100)

@pagy, @messages = pagy(:offset, Message.order(created_at: :desc), limit: limit)

render json: {
data: @messages,
pagination: @pagy.data_hash
}
end

clamp(1, 100) ensures the value stays between 1 and 100 regardless of what the client sends.

References