Pagination
Almost every API needs to paginate data to avoid returning too much data at once. This is especially true with GraphQL as you can ask for as much data as you want in a single query.
Let's see how we can create an API that paginates data. We'll be creating a new field that returns all the podcasts and paginates them.
Basic pagination
Let's see how we can implement a basic pagination for our API. We'll allow our
users to paginate the podcasts by passing a page
and perPage
argument to our
field.
The query will look like this:
query {
podcasts(page: 1, perPage: 10) {
title
}
}
Our data module exposes a paginate_podcast
function that we can use to
paginate the podcasts. Let's create a new field on our Query that uses this
function. Go to podcasts/query.py
and update PodcastsQuery
to look like
this:
import strawberry
from db import data
from typing import Optional, List
from .types import Podcast
@strawberry.type
class PodcastsQuery:
# keep the previous fields
...
@strawberry.field
async def podcasts(self, page: int = 1, per_page: int = 10) -> List[Podcast]:
db_podcasts = await data.paginate_podcast(page=page, per_page=per_page)
return [
Podcast(
id=db_podcast.id,
title=db_podcast.title,
description=db_podcast.description,
)
for db_podcast in db_podcasts
]
Similar to the previous example, we have created a resolver that uses the data
module to fetch the podcasts from the db and returns a list of Podcast
objects. One thing to note is that we are using default arguments for page
and
per_page
, this means that if the user doesn't pass these arguments, we'll be
using the default values. These defaults are also exposed in GraphQL.
Feel free to go to GraphiQL and try out!
Some basic validation
We can do some basic validation on the page
and per_page
arguments to make
sure that the user is not passing invalid values. Let's update our resolver to
look like this:
import strawberry
from db import data
from typing import Optional, List
from .types import Podcast
@strawberry.type
class PodcastsQuery:
# keep the previous fields
...
@strawberry.field
async def podcasts(self, page: int = 1, per_page: int = 10) -> List[Podcast]:
if page < 1:
raise ValueError("page must be greater than 0")
if per_page < 1:
raise ValueError("per page must be greater than 0")
if per_page > 50:
raise ValueError("per page must be less than 50")
db_podcasts = await data.paginate_podcast(page=page, per_page=per_page)
return [
Podcast(
id=db_podcast.id,
title=db_podcast.title,
description=db_podcast.description,
)
for db_podcast in db_podcasts
]
Now if the user passes a page
or per_page
that is less than 1 or greater
than 50, we'll raise an error. Every exception that is raised in a resolver will
be returned to the user as an error.
Note, there are ways of changing how errors are returned to the user, as returning all the exceptions might be problematic in some cases.
Returning has next page
We can also let our clients know if there's more pages in our paginated field.
We can do this by returning a has_next_page
field in our resolver. We can do
this by creating a new type that contains the has_next_page
field and the list
of podcasts.
Let's create a new type called PaginatedPodcasts
that contains the list of
podcasts and the total number of items:
import strawberry
from typing import List
@strawberry.type
class PaginatedPodcasts:
items: List[Podcast]
has_next_page: bool
We can now update our resolver to return a PaginatedPodcasts
object:
import strawberry
@strawberry.type
class PodcastsQuery:
# keep the previous fields
...
@strawberry.field
async def podcasts(self, page: int = 1, per_page: int = 10) -> PaginatedPodcasts:
# ... same validation above
db_podcasts = await data.paginate_podcast(page=page, per_page=per_page)
return PaginatedPodcasts(
items=[
Podcast(
id=db_podcast.id,
title=db_podcast.title,
description=db_podcast.description,
)
for db_podcast in db_podcasts
],
has_next_page=db_podcasts.has_next(),
)
We have now updated our resolver to return a PaginatedPodcasts
object instead
of a list of podcasts. We are also returning a has_next_page
field that tells
the user if there's more pages.
We can now test this by going to GraphiQL and running the following query:
query {
podcasts(page: 1, perPage: 1) {
items {
title
}
hasNextPage
}
}
This pagination works well for small datasets, but might not work super well for large one. Another approach could be to use a cursor based pagination. We'll also see a specification called Relay that allows us to create a cursor based pagination and it is quite popular in the GraphQL ecosystem.