Skip to content

Pagination

Pagination is essential for managing large datasets in GraphQL APIs. django-graphex provides several pagination strategies to efficiently handle query results and improve performance.

Pagination Types

django-graphex offers three pagination implementations:

  • :material-format-list-numbered: LimitOffsetGraphqlPagination: Traditional limit/offset pagination
  • :material-book-open-page-variant: PageGraphqlPagination: Page-number based pagination
  • :material-cursor-default: CursorGraphqlPagination: Forward keyset (cursor) pagination with pageInfo

LimitOffsetGraphqlPagination

The most common pagination method, using limit and offset parameters to control result sets.

Features

  • :material-speedometer: Simple & Fast: Easy to understand and implement
  • :material-sort: Flexible Ordering: Supports custom ordering with Django syntax
  • :material-tune: Configurable Limits: Set default and maximum page sizes
  • :material-database: Database Efficient: Works well with Django QuerySets

Basic Usage

from django_graphex.paginations import LimitOffsetGraphqlPagination

# Basic configuration
pagination = LimitOffsetGraphqlPagination(
    default_limit=25,    # Default number of items per page
    max_limit=100,       # Maximum allowed limit
    ordering="-id"       # Default ordering
)
from django_graphex import DjangoListObjectType
from .models import User

class UserListType(DjangoListObjectType):
    class Meta:
        model = User
        pagination = LimitOffsetGraphqlPagination(
            default_limit=25,
            max_limit=100,
            ordering="-date_joined"
        )
from django_graphex import DjangoFilterPaginateListField
from .types import UserType

class Query(graphene.ObjectType):
    users = DjangoFilterPaginateListField(
        UserType,
        pagination=LimitOffsetGraphqlPagination(default_limit=10)
    )

Configuration Options

LimitOffsetGraphqlPagination(
    default_limit=20,                    # Default items per page
    max_limit=100,                      # Maximum allowed limit
    ordering="-created_at",             # Default ordering field(s)
    limit_query_param="limit",          # GraphQL argument name for limit
    offset_query_param="offset",        # GraphQL argument name for offset
    ordering_param="ordering"           # GraphQL argument name for ordering
)

Query Examples

Argument placement

Pagination and ordering arguments (limit, offset, ordering) live on the results subfield. Filter arguments live on the list field. totalCount is a sibling of results.

query GetUsers {
  users {
    results {
      id
      username
      email
    }
    totalCount
  }
}
query GetUsersWithPagination {
  users {
    results(limit: 10, offset: 20) {
      id
      username
      email
    }
    totalCount
  }
}
query GetUsersOrdered {
  users {
    results(limit: 10, ordering: "username,-date_joined") {
      id
      username
      email
      dateJoined
    }
    totalCount
  }
}

Response Structure

{
  "data": {
    "users": {
      "totalCount": 150,
      "results": [
        {
          "id": "1",
          "username": "john_doe",
          "email": "john@example.com"
        },
        {
          "id": "2",
          "username": "jane_smith",
          "email": "jane@example.com"
        }
      ]
    }
  }
}

PageGraphqlPagination

Page-number based pagination, similar to Django's built-in pagination.

Features

  • :material-book-multiple: Page-Based: Navigate by page numbers
  • :material-resize: Dynamic Page Size: Optional client-controlled page sizes
  • :material-calculator: Automatic Calculation: Handles page calculations automatically
  • :material-navigation: User Friendly: Intuitive for frontend pagination controls

Basic Usage

from django_graphex.paginations import PageGraphqlPagination

pagination = PageGraphqlPagination(
    page_size=25,                    # Items per page
    page_size_query_param="pageSize", # Allow client to control page size
    max_page_size=100,               # Maximum page size
    ordering="-created_at"           # Default ordering
)
class UserListType(DjangoListObjectType):
    class Meta:
        model = User
        pagination = PageGraphqlPagination(
            page_size=20,
            page_size_query_param="pageSize",
            max_page_size=100
        )

Configuration Options

PageGraphqlPagination(
    page_size=25,                       # Default page size
    page_size_query_param="pageSize",   # Enable dynamic page sizing
    max_page_size=100,                  # Maximum allowed page size
    ordering="-id",                     # Default ordering
    ordering_param="ordering"           # Ordering parameter name
)

Dynamic Page Size

Set page_size_query_param to allow clients to control page size. If not set, page size is fixed.

Query Examples

query GetUsersPage {
  users {
    results(page: 1) {
      id
      username
      email
    }
    totalCount
  }
}
query GetUsersWithPageSize {
  users {
    results(page: 2, pageSize: 15) {
      id
      username
      email
    }
    totalCount
  }
}
query GetUsersForPagination {
  users {
    results(page: 3, pageSize: 20, ordering: "username") {
      id
      username
      email
      dateJoined
    }
    totalCount
    # Calculate pagination info on frontend:
    # totalPages = Math.ceil(totalCount / pageSize)
    # hasNextPage = page < totalPages
    # hasPreviousPage = page > 1
  }
}

CursorGraphqlPagination

Forward keyset (cursor) pagination over a single ordering field. Instead of an offset (which gets slow on large tables and skips/repeats rows when data changes), each page is fetched relative to the ordering value of the previous page's last row — so it stays fast and stable. The list type also exposes a pageInfo field, so the client just echoes endCursor to get the next page.

Features

  • :material-flash: Constant-time paging regardless of how deep you are (no large OFFSET).
  • :material-shield-check: Stable under inserts/deletes between pages.
  • :material-information: pageInfo with endCursor / hasNextPage / hasPreviousPage / startCursor.

Basic Usage

import graphene
from django_graphex import (
    DjangoListObjectType,
    DjangoListObjectField,
    CursorGraphqlPagination,
)
from .models import Event


class EventListType(DjangoListObjectType):
    class Meta:
        model = Event
        description = "Event list with cursor pagination"
        pagination = CursorGraphqlPagination(ordering="id")  # use "-id" for newest-first


class Query(graphene.ObjectType):
    events = DjangoListObjectField(EventListType)

Query Examples

Paginate forward with variables — write first/cursor once and pass them to both results and pageInfo:

query Events($first: Int!, $cursor: String) {
  events {
    results(first: $first, cursor: $cursor) {
      id
      name
    }
    totalCount
    pageInfo(first: $first, cursor: $cursor) {
      startCursor
      endCursor
      hasNextPage
      hasPreviousPage
    }
  }
}
{ "first": 3 }
{
  "data": {
    "events": {
      "results": [
        { "id": "1", "name": "Item 00" },
        { "id": "2", "name": "Item 01" },
        { "id": "3", "name": "Item 02" }
      ],
      "totalCount": 12,
      "pageInfo": {
        "startCursor": "Y3Vyc29yOjE=",
        "endCursor": "Y3Vyc29yOjM=",
        "hasNextPage": true,
        "hasPreviousPage": false
      }
    }
  }
}
{ "first": 3, "cursor": "Y3Vyc29yOjM=" }
{
  "data": {
    "events": {
      "results": [
        { "id": "4", "name": "Item 03" },
        { "id": "5", "name": "Item 04" },
        { "id": "6", "name": "Item 05" }
      ],
      "pageInfo": {
        "startCursor": "Y3Vyc29yOjQ=",
        "endCursor": "Y3Vyc29yOjY=",
        "hasNextPage": true,
        "hasPreviousPage": true
      }
    }
  }
}

How to navigate

  • Next page: send the previous pageInfo.endCursor as cursor.
  • Stop: when pageInfo.hasNextPage is false.
  • hasPreviousPage is exact (there really is a row before the current page), so it is false on the first page even if a stray cursor is supplied.

Scope

Cursor pagination is forward-only (first + cursor); backward pagination (last/before) is intentionally not provided. ordering must be a single field (a leading - selects descending order) — order by a stable, indexed field such as the primary key.

Advanced Pagination Usage

Multiple Ordering Fields

Both pagination types support multiple ordering fields:

query {
  users {
    results(ordering: "last_name,first_name,-date_joined") {
      firstName
      lastName
      dateJoined
    }
    totalCount
  }
}
# This GraphQL query is equivalent to:
User.objects.order_by('last_name', 'first_name', '-date_joined')

Combining with Filtering

Pagination works seamlessly with filtering:

class UserListType(DjangoListObjectType):
    class Meta:
        model = User
        pagination = LimitOffsetGraphqlPagination(default_limit=25)
        filter_fields = {
            'username': ('icontains', 'exact'),
            'email': ('icontains', 'exact'),
            'is_active': ('exact',),
        }
query GetActiveUsers {
  users(filter: { isActive: { exact: true }, username: { icontains: "john" } }) {
    results(limit: 10, ordering: "username") {
      id
      username
      email
      isActive
    }
    totalCount
  }
}

Custom Pagination Classes

Create custom pagination for specific needs:

from django_graphex.paginations import LimitOffsetGraphqlPagination

class CustomPagination(LimitOffsetGraphqlPagination):
    def __init__(self, **kwargs):
        super().__init__(
            default_limit=50,
            max_limit=200,
            ordering="-updated_at",
            **kwargs
        )
from django_graphex.paginations import PageGraphqlPagination

class LargeDatasetPagination(PageGraphqlPagination):
    def __init__(self, **kwargs):
        super().__init__(
            page_size=100,
            page_size_query_param=None,  # Fixed page size
            max_page_size=100,
            ordering="-id",
            **kwargs
        )

Performance Considerations

Database Query Optimization

Count Queries

Pagination requires COUNT queries which can be expensive on large datasets. Consider caching count results for better performance.

# ✅ Good: Use indexed fields for ordering
pagination = LimitOffsetGraphqlPagination(
    ordering="-id"  # Primary key is indexed
)

# ⚠️  Less efficient: Non-indexed field
pagination = LimitOffsetGraphqlPagination(
    ordering="full_name"  # May not be indexed
)
# Optimize with select_related for foreign keys via Meta.queryset
class UserListType(DjangoListObjectType):
    class Meta:
        model = User
        pagination = LimitOffsetGraphqlPagination(default_limit=25)
        queryset = User.objects.select_related('profile')

Large Offset Performance

Offset Limitations

Large offsets (e.g., offset=10000) can be slow. Consider cursor-based pagination for very large datasets.

Frontend Integration

React Example with Apollo Client

import { gql, useQuery } from '@apollo/client';

const GET_USERS = gql`
  query GetUsers($limit: Int!, $offset: Int!) {
    users {
      results(limit: $limit, offset: $offset) {
        id
        username
        email
      }
      totalCount
    }
  }
`;

function UserList() {
  const [page, setPage] = useState(0);
  const limit = 10;
  const offset = page * limit;

  const { loading, error, data } = useQuery(GET_USERS, {
    variables: { limit, offset }
  });

  const totalPages = data ? Math.ceil(data.users.totalCount / limit) : 0;

  return (
    <div>
      {data?.users.results.map(user => (
        <div key={user.id}>{user.username}</div>
      ))}

      <Pagination
        currentPage={page}
        totalPages={totalPages}
        onPageChange={setPage}
      />
    </div>
  );
}
const GET_USERS_BY_PAGE = gql`
  query GetUsers($page: Int!, $pageSize: Int) {
    users {
      results(page: $page, pageSize: $pageSize) {
        id
        username
        email
      }
      totalCount
    }
  }
`;

function UserList() {
  const [currentPage, setCurrentPage] = useState(1);
  const pageSize = 15;

  const { loading, error, data } = useQuery(GET_USERS_BY_PAGE, {
    variables: { page: currentPage, pageSize }
  });

  const totalPages = data ? Math.ceil(data.users.totalCount / pageSize) : 0;

  return (
    <div>
      {data?.users.results.map(user => (
        <div key={user.id}>{user.username}</div>
      ))}

      <div>
        <button
          disabled={currentPage <= 1}
          onClick={() => setCurrentPage(currentPage - 1)}
        >
          Previous
        </button>

        <span>Page {currentPage} of {totalPages}</span>

        <button
          disabled={currentPage >= totalPages}
          onClick={() => setCurrentPage(currentPage + 1)}
        >
          Next
        </button>
      </div>
    </div>
  );
}

Best Practices

Pagination Best Practices

  1. Set Reasonable Defaults: Use sensible default page sizes (10-50 items)
  2. Limit Maximum Size: Prevent excessive data transfer with max limits
  3. Use Indexed Fields: Order by indexed fields for better performance
  4. Cache Counts: Cache total counts for frequently accessed datasets
  5. Consider Cursor Pagination: For real-time data or very large datasets
  6. Frontend State Management: Maintain pagination state in your frontend

Security Considerations

# Limit maximum page sizes to prevent abuse
pagination = LimitOffsetGraphqlPagination(
    default_limit=25,
    max_limit=100,  # Prevent users from requesting thousands of records
    ordering="-id"
)

Testing Pagination

import pytest
from graphene.test import Client
from .schema import schema

@pytest.mark.django_db
def test_users_pagination():
    # Create test users
    for i in range(50):
        User.objects.create_user(
            username=f'user{i}',
            email=f'user{i}@example.com'
        )

    client = Client(schema)
    query = """
        query GetUsers($limit: Int!, $offset: Int!) {
            users {
                results(limit: $limit, offset: $offset) {
                    id
                    username
                }
                totalCount
            }
        }
    """

    result = client.execute(query, variables={'limit': 10, 'offset': 20})

    assert len(result['data']['users']['results']) == 10
    assert result['data']['users']['totalCount'] == 50
@pytest.mark.django_db
def test_users_ordering():
    User.objects.create_user(username='charlie', email='c@example.com')
    User.objects.create_user(username='alice', email='a@example.com')
    User.objects.create_user(username='bob', email='b@example.com')

    client = Client(schema)
    query = """
        query GetUsers($ordering: String!) {
            users {
                results(ordering: $ordering, limit: 10) {
                    username
                }
            }
        }
    """

    result = client.execute(query, variables={'ordering': 'username'})
    usernames = [user['username'] for user in result['data']['users']['results']]

    assert usernames == ['alice', 'bob', 'charlie']

The pagination system in django-graphex provides flexible, efficient ways to handle large datasets while maintaining good performance and user experience.