Implementing Offset-Based Pagination

Make sure to check our introduction to pagination here !

Let us implement offset-based pagination in GraphQL. By the end of this tutorial, we should be able to return a sorted, filtered, and paginated list of users.

Let us model the User type, which represents one user, with a name, occupation, and age.

example.py
from typing import List, TypeVar, Dict, Any, Generic
import strawberry
@strawberry.type
class User:
name: str = strawberry.field(description="The name of the user.")
occupation: str = strawberry.field(description="The occupation of the user.")
age: int = strawberry.field(description="The age of the user.")
@staticmethod
def from_row(row: Dict[str, Any]):
return User(name=row["name"], occupation=row["occupation"], age=row["age"])

Let us now model the PaginationWindow , which represents one β€œslice” of sorted, filtered, and paginated items.

Item = TypeVar("Item")
@strawberry.type
class PaginationWindow(Generic[Item]):
items: List[Item] = strawberry.field(
description="The list of items in this pagination window."
)
total_items_count: int = strawberry.field(
description="Total number of items in the filtered dataset."
)

Note that PaginationWindow is generic - it can represent a slice of users, or a slice of any other type of items that we might want to paginate.

PaginationWindow also contains total_items_count , which specifies how many items there are in total in the filtered dataset, so that the client knows what the highest offset value can be.

Let’s define the query:

@strawberry.type
class Query:
@strawberry.field(description="Get a list of users.")
def users(
self,
order_by: str,
limit: int,
offset: int = 0,
name: str | None = None,
occupation: str | None = None,
) -> PaginationWindow[User]:
filters = {}
if name:
filters["name"] = name
if occupation:
filters["occupation"] = occupation
return get_pagination_window(
dataset=user_data,
ItemType=User,
order_by=order_by,
limit=limit,
offset=offset,
filters=filters,
)
schema = strawberry.Schema(query=Query)

Now we’ll define a mock dataset and implement the get_pagination_window function, which is used by the users query.

For the sake of simplicity, our dataset will be an in-memory list containing four users:

user_data = [
{
"id": 1,
"name": "Norman Osborn",
"occupation": "Founder, Oscorp Industries",
"age": 42,
},
{
"id": 2,
"name": "Peter Parker",
"occupation": "Freelance Photographer, The Daily Bugle",
"age": 20,
},
{
"id": 3,
"name": "Harold Osborn",
"occupation": "President, Oscorp Industries",
"age": 19,
},
{
"id": 4,
"name": "Eddie Brock",
"occupation": "Journalist, The Eddie Brock Report",
"age": 20,
},
]

Here’s the implementation of the get_pagination_window function. Note that it is generic and should work for all item types, not only for the User type.

def get_pagination_window(
dataset: List[GenericType],
ItemType: type,
order_by: str,
limit: int,
offset: int = 0,
filters: dict[str, str] = {},
) -> PaginationWindow:
"""
Get one pagination window on the given dataset for the given limit
and offset, ordered by the given attribute and filtered using the
given filters
"""
if limit <= 0 or limit > 100:
raise Exception(f"limit ({limit}) must be between 0-100")
if filters:
dataset = list(filter(lambda x: matches(x, filters), dataset))
dataset.sort(key=lambda x: x[order_by])
if offset != 0 and not 0 <= offset < len(dataset):
raise Exception(f"offset ({offset}) is out of range " f"(0-{len(dataset) - 1})")
total_items_count = len(dataset)
items = dataset[offset : offset + limit]
items = [ItemType.from_row(x) for x in items]
return PaginationWindow(items=items, total_items_count=total_items_count)
def matches(item, filters):
"""
Test whether the item matches the given filters.
This demo only supports filtering by string fields.
"""
for attr_name, val in filters.items():
if val not in item[attr_name]:
return False
return True

The above code first filters the dataset according to the given filters, then sorts the dataset according to the given order_by field.

It then calculates total_items_count (this must be done after filtering), and then slices the relevant items according to offset and limit .

Finally, it converts the items to the given strawberry type, and returns a PaginationWindow containing these items, as well as the total_items_count .

In a real project, you would probably replace this with code that fetches from a database using offset and limit .

Tip

If you’re using Strawberry with the Django web framework, you might want to make use of the Django pagination API. You can check it out here .

Running the Query

Now, let us start the server and see offset-based pagination in action!

Terminal window
strawberry server example:schema

You will get the following message:

Running strawberry on http://0.0.0.0:8000/graphql πŸ“

Go to http://0.0.0.0:8000/graphql to open GraphiQL, and run the following query to get first two users, ordered by name:

{
users(orderBy: "name", offset: 0, limit: 2) {
items {
name
age
occupation
}
totalItemsCount
}
}

The result should look like this:

{
"data": {
"users": {
"items": [
{
"name": "Eddie Brock",
"age": 20,
"occupation": "Journalist, The Eddie Brock Report"
},
{
"name": "Harold Osborn",
"age": 19,
"occupation": "President, Oscorp Industries"
}
],
"totalItemsCount": 4
}
}
}

The result contains:

Get the next page of users by running the same query, after incrementing offset by limit .

Repeat until offset reaches totalItemsCount .

Running a Filtered Query

Let’s run the query again, but this time we’ll filter out some users based on their occupation.

{
users(orderBy: "name", offset: 0, limit: 2, occupation: "ie") {
items {
name
age
occupation
}
totalItemsCount
}
}

By supplying occupation: "ie" in the query, we are requesting only users whose occupation contains the substring β€œie”.

This is the result:

{
"data": {
"users": {
"items": [
{
"name": "Eddie Brock",
"age": 20,
"occupation": "Journalist, The Eddie Brock Report"
},
{
"name": "Harold Osborn",
"age": 19,
"occupation": "President, Oscorp Industries"
}
],
"totalItemsCount": 3
}
}
}

Note that totalItemsCount is now 3 and not 4, because only 3 users in total match the filter.