module CursorPagination
extend ActiveSupport::Concern
private
def paginate_with_cursor(scope, per_page: 20)
cursor = params[:cursor]
direction = params[:direction] || 'next'
query = scope.limit(per_page + 1)
if cursor.present?
decoded_cursor = decode_cursor(cursor)
if direction == 'next'
query = query.where('id > ?', decoded_cursor)
else
query = query.where('id < ?', decoded_cursor).reverse_order
end
end
records = query.to_a
has_more = records.size > per_page
records = records.take(per_page)
{
data: records,
meta: {
next_cursor: has_more ? encode_cursor(records.last.id) : nil,
prev_cursor: cursor.present? ? encode_cursor(records.first.id) : nil,
has_more: has_more
}
}
end
def encode_cursor(id)
Base64.strict_encode64(id.to_s)
end
def decode_cursor(cursor)
Base64.strict_decode64(cursor).to_i
end
end
Traditional offset-based pagination becomes unreliable and slow for large datasets when records are frequently inserted or deleted—users can miss items or see duplicates across pages. Cursor-based pagination solves this by using an opaque token that encodes the position in the dataset, typically the id of the last seen record. Each response includes next_cursor and prev_cursor fields that clients pass back to fetch subsequent pages. This approach delivers consistent results even when the underlying data changes between requests, and it performs well at scale because it uses indexed lookups rather than counting offsets. I encode cursors with Base64 to hide implementation details and include pagination metadata in a top-level meta object.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Share this code
Here's the card — post it anywhere.