Offset based pagination in GraphQL-ruby
Offset-based pagination is a straightforward pagination method that uses limit and offset parameters to fetch subsets of data. While simpler than cursor-based pagination, it's useful for many use cases.
Basic Implementation
Add Arguments to Query
Add limit and offset arguments:
# app/graphql/types/query_type.rb
module Types
class QueryType < Types::BaseObject
field :posts, [Types::PostType], null: false do
argument :limit, Integer, required: false, default_value: 20
argument :offset, Integer, required: false, default_value: 0
end
def posts(limit: 20, offset: 0)
Post.order(created_at: :desc)
.limit(limit)
.offset(offset)
end
end
end
Query Example
query GetPosts($limit: Int, $offset: Int) {
posts(limit: $limit, offset: $offset) {
id
title
content
createdAt
}
}
Input Object Pattern
Create Input Type
Use input objects for cleaner API:
# app/graphql/types/pagination_input.rb
module Types
class PaginationInput < Types::BaseInputObject
argument :limit, Integer, required: false, default_value: 20
argument :offset, Integer, required: false, default_value: 0
end
end
Use in Query
field :posts, [Types::PostType], null: false do
argument :pagination, Types::PaginationInput, required: false
end
def posts(pagination: { limit: 20, offset: 0 })
limit = pagination[:limit] || 20
offset = pagination[:offset] || 0
Post.order(created_at: :desc)
.limit(limit)
.offset(offset)
end
Query with Input Object
query GetPosts($pagination: PaginationInput) {
posts(pagination: $pagination) {
id
title
content
}
}
Adding Page Info
Return Pagination Metadata
Include pagination information in the response:
# app/graphql/types/post_connection_type.rb
module Types
class PostConnectionType < Types::BaseObject
field :posts, [Types::PostType], null: false
field :total_count, Integer, null: false
field :has_next_page, Boolean, null: false
field :has_previous_page, Boolean, null: false
field :current_page, Integer, null: false
field :total_pages, Integer, null: false
end
end
Implement Resolver
field :posts, Types::PostConnectionType, null: false do
argument :limit, Integer, required: false, default_value: 20
argument :offset, Integer, required: false, default_value: 0
end
def posts(limit: 20, offset: 0)
total_count = Post.count
posts = Post.order(created_at: :desc)
.limit(limit)
.offset(offset)
current_page = (offset / limit) + 1
total_pages = (total_count.to_f / limit).ceil
{
posts: posts,
total_count: total_count,
has_next_page: (offset + limit) < total_count,
has_previous_page: offset > 0,
current_page: current_page,
total_pages: total_pages
}
end
Query with Page Info
query GetPosts($limit: Int, $offset: Int) {
posts(limit: $limit, offset: $offset) {
posts {
id
title
content
}
totalCount
hasNextPage
hasPreviousPage
currentPage
totalPages
}
}
Real-World Example
With Filtering
Combine pagination with filtering:
field :posts, Types::PostConnectionType, null: false do
argument :limit, Integer, required: false, default_value: 20
argument :offset, Integer, required: false, default_value: 0
argument :status, String, required: false
argument :category, String, required: false
end
def posts(limit: 20, offset: 0, status: nil, category: nil)
relation = Post.order(created_at: :desc)
relation = relation.where(status: status) if status
relation = relation.joins(:category).where(categories: { name: category }) if category
total_count = relation.count
posts = relation.limit(limit).offset(offset)
{
posts: posts,
total_count: total_count,
has_next_page: (offset + limit) < total_count,
has_previous_page: offset > 0,
current_page: (offset / limit) + 1,
total_pages: (total_count.to_f / limit).ceil
}
end
Advantages
Offset-based pagination is simple and intuitive:
- Easy to implement: Straightforward limit/offset logic
- Predictable: Easy to calculate page numbers
- User-friendly: Familiar pagination UI (page 1, 2, 3...)
- Good for small datasets: Works well when data doesn't change frequently
Limitations
Be aware of the limitations:
- Performance: Degrades with large offsets
- Inconsistency: Can skip or duplicate items if data changes
- Not scalable: Poor performance at high offsets
When to Use
Use offset-based pagination when:
- Dataset is relatively small
- Data doesn't change frequently
- You need page numbers for UI
- Simplicity is more important than performance
Best Practices
- Set reasonable limits: Default to 20-50 items per page
- Validate inputs: Ensure limit and offset are positive
- Add indexes: Index columns used in ordering
- Consider cursor-based: For large or frequently changing data
Input Validation
Validate pagination parameters:
def posts(limit: 20, offset: 0)
limit = [limit.to_i, 100].min.clamp(1, 100) # Max 100 items
offset = [offset.to_i, 0].max # No negative offsets
Post.order(created_at: :desc)
.limit(limit)
.offset(offset)
end
Comparison with Cursor-Based
| Feature | Offset-Based | Cursor-Based |
|---|---|---|
| Simplicity | Very simple | More complex |
| Performance | Good for small offsets | Better overall |
| Consistency | Can have issues | More consistent |
| UI | Page numbers | Infinite scroll |
Conclusion
Offset-based pagination in GraphQL-ruby provides a simple, intuitive way to paginate data. While it has limitations with large datasets, it's perfect for many use cases where simplicity and familiarity are more important than maximum performance. For large or frequently changing datasets, consider cursor-based pagination instead.