loading...

Connection based pagination in GraphQL

zth profile image Gabriel Nordeborn ・9 min read

This series of articles is written by Gabriel Nordeborn and Sean Grove. Gabriel is a frontend developer and partner at the Swedish IT consultancy Arizon, and has been a Relay user for a long time. Sean is a co-founder of OneGraph.com, unifying 3rd-party APIs with GraphQL.

Connection based pagination is a way of structuring how you do pagination so it caters to both simple and a bit more advanced pagination. It was recently promoted to an official GraphQL best practice, and this article will focus on why it’s a good thing in GraphQL, and what type of tooling it enables.

There are numerous of resources about what connection based pagination is and why it’s useful. If you want to get your feet wet before reading this article, we recommend having a look at the official GraphQL website section on pagination.

Standardization is the win

Connection based pagination is all about using a standardized contract for pagination. Now, why would you care about that? Well, as we’ll show you throughout this article, a standardized contract means tooling and frameworks can integrate deeply with it. And that in turn means that your life as a developer will be a whole lot easier. It also means we can ship a lot faster, as our tooling will be doing some heavy lifting for us.

What is it and what is it suitable for?

Connection based pagination is best suited for infinite scroll type pagination, where you keep adding an arbitrary amount of items to an already existing list as you move through the UI. If you’re looking to implement pagination that fetches an entire page of items in isolation rather than continuously adding to an existing list, you’re better off implementing that separately.

The key here is that connection based pagination is just a way of structuring the contract for pagination. While there may be implications for how you fetch your data backend when using connection based pagination, the primary purpose of using it in the GraphQL world is to provide a standardized structure for paginating, rather than committing your backend to a specific pagination strategy.

A standardized structure for pagination

Now, let’s look at an example of the structure that connection based pagination follows. Imagine we’re building a search feature for finding movies we think you'll like. Our search backend is really smart, so we're able to factor in a bunch of things in our assessment of what we think you'd like to watch. The definition below symbolizes fetching and paginating a list of Movie by an arbitrary search term. The search term could be something like action movies, or romcoms. Our search engine can make sense of it all!

Feel free to skim through the spec - don’t spend time trying to understand it now. It’ll just be good to have the general shape in the back of your mind as you read the rest of the article.

    type Movie {
      id: ID!
      name: String!
      releasedYear: Int!
      coverUrl: String!
    }

    # An edge for the Movie type
    type MovieEdge {
      node: Movie # The node, Movie in this case, for this edge
      cursor: String # The cursor for this edge
    }

    # Metadata for the current pagination
    type PageInfo {
      hasNextPage: Boolean!
      endCursor: String
      hasPreviousPage: Boolean!
      startCursor: String
    }

    # The connection, aka the root type of the pagination.
    type MovieConnection {
      pageInfo: PageInfo!
      edges: [MovieEdge]
    }

    type Query {
      # first, last, after and before can be used to paginate forward and backward
      movies(
        query: String!
        first: Int, 
        last: Int, 
        after: String, 
        before: String
      ): MovieConnection
    }

That’s a lot of structure for something that should be really simple, right? Like mentioned above, just keep the structure in the back of your head, and let’s look at a typical scenario of how pagination is implemented and how it can grow in a project.

A common scenario of how pagination is implemented

Most applications need some form of pagination sooner or later, and it can be implemented in sorts of different ways. Let’s paint a scenario of how implementing the most common, run-of-the-mill pagination in GraphQL might look.

Step 1. The “simplest possible pagination”

Building on our movies example above, we’ll design a search function that can paginate movies by an arbitrary search query, like the wild west or intense action.

Most people (understandably!) start out basic: “We just want some very simple pagination, no need to be fancy”. It usually ends up looking something like this:

    type Query {
      searchMovies(
        query: String!
        limit: Int
        offset: Int
      ): [Movie!]
    }

We’re filtering a simple list of Movie using a search query to find the the best matching movies for that search query. This is all fine and work well!

Step 2. How do we know if there are more results?

The product evolves, and we want to add a nice “See more movies”-button to our UI if there are more movies that can be fetched. But, since we’re just returning a list in the example above, we have no way of knowing whether there are more results to load or not.

Well, we can solve this too quite easily! Let’s extend our example. We don’t want to modify an individual Movie (it wouldn’t make sense to have a hasNextPage property there), so we need searchMovies to return a little wrapper object (MovieSearchResult ) where we can put our metadata:

    type MovieSearchResult {
      hasNext: Boolean
      items: [Movie!]
    }

    type Query {
      searchMovies(
        query: String!
        limit: Int
        offset: Int
      ): MovieSearchResult!
    }

Great, this does everything we need. Problem solved.

Step 3. When using offset break down

Our product evolves and we now need to make sure that we can produce a unique URL for continuing pagination from a specific Movie in our results.

The immediate reaction may be to build a URL like this:
/search?limit=${limit}&offset=${offset}&query=${query}

This should work, right? Sadly, not so well.

What happens when movies are added to the database and would end up before the movie we’re currently looking at in the results? The offset wouldn’t point to the movie we want.

Let’s step back and express our intentions like human-beings talking to a backend API maintainer for a second:

I have all of the movie results up to “Frost 2”. I’d like to get the next 10 movies after Frost 2, please!

We’ve described enough of our intentions so that a human would know, “Oh, there have been a few other movies added and some removed that also match your search query, but here are the 10 after Frost 2 specifically.” That’s the behavior we want in our pagination!

For this to work, we’ll need to be able to paginate from a specific item in the results. So naturally we’ll need a way of identifying a single item in the results. There’s a pre-existing concept for that called cursors - we’ll just use that concept (the best artist steal, and all that), and tweak our search query to support it.

    type MovieSearchResultWrapper {
      movie: Movie
      cursor: String
    }

    type MovieSearchResult {
      hasNext: Boolean
      items: [MovieSearchResultWrapper!]
    }

    type Query {
      searchMovies(
        query: String!
        limit: Int
        cursor: String
      ): MovieSearchResult!
    }

We can now query movie's by a cursor instead of just offset, and we’ll have access to the cursor for each result. This will ensure that our pagination is stable (items added before the current item won’t mess up the listing as before). We can now produce a URL like this:

/search?limit=${limit}&cursor=${cursor}&query=${query}

This URL will be stable, and always point to search results starting from the item in the results list (even if several have been added or removed in the meantime). Excellent!

Step 4. Metadata relevant for each individual item in the search result

A new feature request comes in - since we have all this fancy knowledge about how well we think each result both matches your taste and how relevant it is to your query, we naturally want to flex our muscles as show that to the user!

However, this is data that's only really valid for a specific result item with a specific search query. Our search engine also automatically produces that information for us in an efficient way for each search result (since that's what it's already using to order the results). But, where would it be logical to put that information for each result if we don’t want to extend the root Movie type (because that wouldn't really make sense)? Well, we just introduced MovieSearchResultWrapper for our cursor needs, and it happens to keep metadata only relevant to this particular search. Let’s put it there:

    type MovieSearchResultWrapper {
      movie: Movie
      cursor: String
      relevanceFactor: Float
    }

Excellent, we now have a nice number we can turn into some fancy gauge meter and everyone will be happy!

Wait - didn’t we just implement connection based pagination organically?

If you look at what we’ve produced organically here and compare it to the initial structure we drew up with connection based pagination, aren’t these two virtually the same? Yes they are! Let’s fit the model above into a connection structure:

    type Movie {
      id: ID!
      name: String!
      releasedYear: Int!
      coverUrl: String!
    }

    type MovieEdge {
      node: Movie # "node" is a generalized name for an item in a collection
      cursor: String # The cursor for this result item fits naturally here
      relevanceFactor: Float # More metadata only relevant to this result fits here too
    }

    # All the meta data we'll need to continue paginating
    type PageInfo {
      hasNextPage: Boolean!
      endCursor: String
      hasPreviousPage: Boolean!
      startCursor: String
    }

    # The root type for the pagination. Again, generalized/standardized name
    type MovieConnection {
      pageInfo: PageInfo!
      edges: [MovieEdge]
    }

    type Query {
      searchMovies(
        query: String!
        first: Int, 
        last: Int, 
        after: String, 
        before: String
      ): MovieConnection
    }

So, instead of using our own naming schemes, we can easily fit our pagination into the connection based model. But what’s the benefit of doing that? Maybe you disagree with the naming, think it’s too complicated, or you’re at the stage where simple pagination’s the only thing that’s needed for the foreseeable future. Why care?

Templating removes decisions so you can focus on other stuff that sparks joy(tm)

It turns our we can take our schema above and template-ize it, so any time we want to paginate a list of Grungle, we can just fill in the holes:

    type `${nounWeWantToPaginate} {
      ${...noun fields}
    }

    type ${noun}Edge {
      node: ${noun} # "node" is a generalized name for an item in a collection - it's our noun!
      cursor: String # The cursor for this result item fits naturally here
      ${... any bits of pagination metadata for our noun}
    }
    # All the meta data we'll need to continue paginating
    # This stays the same!
    type PageInfo {
      hasNextPage: Boolean!
      endCursor: String
      hasPreviousPage: Boolean!
      startCursor: String
    }

    # The root type for the pagination. Again, generalized/standardized name
    type ${noun}Connection {
      pageInfo: PageInfo!
      edges: [${noun}Edge]
    }

    type Query {
      ${noun}s(
        first: Int, 
        last: Int, 
        after: String, 
        before: String
      ): ${noun}Connection
    }

Having a template like this makes it much easier to just drop in pagination for a list of our ${noun}

Standardization == tooling can make your life much easier

More importantly though, conventions what we’ve just built up make it easy for tooling to simplify our lives. Here are a few benefits that connection based pagination bring, and what tooling can do for you:

Future proofed pagination
Because the structure provided for connection based pagination give you a logical place to put the things you might eventually need when paginating (like cursors, metadata about additional results and metadata about this specific result item in this specific search), you’re actually future proofing your API when implementing connection based pagination. Even if you don’t need any of the more advanced features we've talked about initially, adding them when you do will be easy since the structure is already there.

Generalized tooling
It’s easy to write generalized tooling. For example, a function that take any ${noun}Connection and return a list of the nodes in the connection, letting you avoid dealing with the somewhat complex structure of connections manually.

It’s also easy to build tooling on the backend. The same thing applies there - it’s standardized, so you can build generic tooling for it. For example, the template we saw above can be turned into a single makeNounPaginator(…) so even making pagination on the backend is easier!

Allow frameworks to do all the heavy lifting for you
But, as frontend developers, perhaps the best part of it all is that if it’s standardized, a framework can build on top of it. Relay is an excellent example of this. Pagination in Relay is incredibly ergonomic and easy when you follow the connection spec. Here’s an article explaining pagination in Relay and what make it great that we really encourage you to read.

Wrapping up

We hope you’ve gotten a feel for what connection based pagination is and why it makes sense by reading this article. Please feel free to drop us a line in the comments below if you have any questions or comments.

Also, as mentioned above, you should really continue by reading our article on pagination in Relay. Relay leverages what we've talked about here to create a superb developer experience for doing pagination, and is a great example of exactly the type of benefits we mean a standardized structure like connection based pagination enables.

If you're a member on Egghead and want to see this article in an 11-minute video form, check out Nik Graf's excellent Paginate Entries using the Connection Specification lesson!

Discussion

pic
Editor guide
Collapse
kazekyo profile image
Kyohei Nakamori

Great article, thank you for writing it up!

If you're a backend engineer and looking at this comment, be careful of generating cursors. Many of the GraphQL frameworks used on the backend convert offsets to cursors, so they can't automatically generate stable pagination.
graphql-relay-js uses offsets, and many frameworks follow it. Of course, graphql-relay-js says "It uses array offsets as pagination, so pagination will only work if the array is static."
github.com/graphql/graphql-relay-j...

To make pagination stable, I think the cursor needs to be persistent; something like an id.
But we have to write code that sorts the list by some column and uses an id to get 10 items from the middle of the list. Some DBs allow you to do this easily, and some DBs make it very difficult for you to do it.
Also, we should think about what happens if you generate a cursor from an id and then the data is deleted.
Anyway, this isn't easy.

*What is explained in this article is correct, but I just wanted to share the point above.

Collapse
n1ru4l profile image
Laurin Quast

Great Article! And great seriesI would love to see more content on what data people encode in their cursors for doing the pagination and how a backend implementation of such a connection would look like.