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.