Relay-Style Connections and Pagination

Common questions around Relay's Connection specification


Relay is an opinionated GraphQL client, and its associated Connections specification defines a pattern for expressing one-to-many relationships in a GraphQL schema:

GraphQL
1query MyPosts($cursor: String) {
2  viewer {
3    posts(first: 5, after: $cursor) {
4      edges {
5        node {
6          id
7          title
8          content
9        }
10        cursor
11      }
12      pageInfo {
13        hasNextPage
14        endCursor
15      }
16    }
17  }
18}
View the GraphQL schema for the above operation
GraphQL
1type Query {
2  viewer: User
3}
4
5type User {
6  id: ID!
7  posts(first: Int, after: String, last: Int, before: String): PostConnection!
8}
9
10"""
11This wrapper type contains the list of "edges" and
12pagination metadata.
13"""
14type PostConnection {
15  edges: [PostEdge!]!
16  pageInfo: PageInfo!
17}
18
19"""
20The "edge" wrapper contains metadata about the item in the
21list. By default it's just a cursor indicating the position
22of the item in the list, but additional metadata is allowed.
23"""
24type PostEdge {
25  """
26  The "node" is the actual item in the list.
27  """
28  node: Post
29  cursor: String!
30}
31
32type Post {
33  id: ID!
34  title: String
35  content: String
36}
37
38"""
39The specification includes the pagination metadata
40in a common type.
41"""
42type PageInfo {
43  hasNextPage: Boolean!
44  hasPreviousPage: Boolean!
45  startCursor: String
46  endCursor: String
47}

It's worth noting that Facebook designed the Connections specification for their Newsfeed feature with these features in mind:

  • It uses cursor-based pagination.

  • It supports paging backward (with the before cursor) and forward (with the after cursor).

  • Each item in the list has a cursor you can use to jump to a specific page in the middle of the list.

These features might not perfectly meet your requirements or the capabilities of your downstream data sources.

Do I have to use Relay-style connections?

No, unless you're using the Relay client. But its popularity outside of the Relay ecosystem is worth taking advantage of:

  • Many developers are familiar with the connection pattern.

  • It encapsulates several schema design best practices.

  • It's designed to be future-proof and support gradual evolution of your GraphQL schema.

Do I even need a wrapper type for my lists?

Consider the "Zero, One, Infinity" rule —can you definitively assert that your list will never require pagination or other metadata about the relationship?

Using a wrapper type for lists provides the following benefits:

  • Avoid breaking changes: You can initially return a wrapper type that doesn't use pagination, and then add pagination later without breaking existing clients. If you return a list directly, you can't add pagination metadata later.

  • Represent entity relationships: The Connection and Edge wrapper types support fields that model attributes of the relationship between entities that don't belong in the entities themselves.

    Consider this example of a many-to-many relationship between Business and Customer:

    GraphQL
    1type Business {
    2  id: ID
    3  customers: CustomerConnection
    4}
    5
    6type CustomerConnection {
    7  edges: [CustomerEdge]
    8  total: Int
    9}
    10
    11type CustomerEdge {
    12  node: Customer
    13  type: CustomerType #highlight-line
    14}
    15
    16enum CustomerType {
    17  IN_STORE
    18  ONLINE
    19  MULTI_CHANNEL
    20}
    21
    22type Customer {
    23  id: ID
    24  shopsAt: BusinessConnection # --snip --
    25}

    A specific Customer might shop at one business IN_STORE and another ONLINE. The type is an attribute of the relationship, not the business or customer itself. Without wrapper types, you don't have a place to put this data.

Do I have to implement the entire connection specification?

No, you can use a subset of the specification. You can implement additional parts over time to reach full compliance with the specification (if necessary).

If your downstream data sources don't support paging backward, you limit your implementation to forward pagination:

An example with only forward pagination
GraphQL
1query MyPosts($cursor: String) {
2  viewer {
3    posts(first: 5, after: $cursor) {
4      edges {
5        node {
6          id
7          title
8          content
9        }
10        cursor
11      }
12      pageInfo {
13        hasNextPage
14        endCursor
15      }
16    }
17  }
18}
GraphQL
1type Query {
2  viewer: User
3}
4
5type User {
6  id: ID!
7  posts(first: Int, after: String): PostConnection!
8}
9
10type PostConnection {
11  edges: [PostEdge!]!
12  pageInfo: PageInfo!
13}
14
15type PostEdge {
16  node: Post
17  cursor: String!
18}
19
20type Post {
21  id: ID!
22  title: String
23  content: String
24}
25
26type PageInfo {
27  hasNextPage: Boolean!
28  endCursor: String
29}

If your downstream data sources don't support per-node cursors, you can drop the edges field and use nodes:

An example without edges
GraphQL
1query MyPosts($cursor: String) {
2  viewer {
3    posts(first: 5, after: $cursor) {
4      nodes {
5        id
6        title
7        content
8      }
9      pageInfo {
10        hasNextPage
11        endCursor
12      }
13    }
14  }
15}
GraphQL
1type Query {
2  viewer: User
3}
4
5type User {
6  id: ID!
7  posts(first: Int, after: String, last: Int, before: String): PostConnection!
8}
9
10type PostConnection {
11  nodes: [Post!]!
12  pageInfo: PageInfo!
13}
14
15type Post {
16  id: ID!
17  title: String
18  content: String
19}
20
21type PageInfo {
22  hasNextPage: Boolean!
23  hasPreviousPage: Boolean!
24  startCursor: String
25  endCursor: String
26}

Are there other ways to design my schema for pagination?

Yes. If your requirements or downstream capabilities don't fit the Relay-style connections spec, we recommend using a visibly different set of conventions so that it's clear to graph consumers that they shouldn't expect to use Relay connection patterns.

Here's an example of pagination that uses page offsets and supports a UI for jumping to a specific page:

An alternative to Relay-style connections
GraphQL
1query MyPosts($page: Int) {
2  viewer {
3    posts(page: $page) {
4      nodes {
5        id
6        title
7        content
8      }
9      totalPages
10    }
11  }
12}
GraphQL
1type Query {
2  viewer: User
3}
4
5type User {
6  id: ID!
7  posts(page: Int): PostsPaginated!
8}
9
10"""
11This wrapper type uses a different suffix to distinguish
12it from Relay-style connection wrappers.
13"""
14type PostsPaginated {
15  """
16  Using `nodes` avoids the redundant
17  `{ posts { posts { id } } }` selection set.
18  """
19  nodes: [Post!]!
20
21  """
22  Adding pagination metadata directly to the
23  wrapper type works well.
24  """
25  totalPages: Int!
26}
27
28type Post {
29  id: ID!
30  title: String
31  content: String
32}

Can I use Relay-style connections with Apollo Federation?

Yes! You define the schema and resolvers for the connection relationship within a single subgraph, so federation has almost no ramifications on the pattern.

The one exception is the PageInfo type, which commonly has a consistent definition for all connections. You must mark this type's definition as @shareable to define it in multiple subgraphs:

GraphQL
1type PageInfo @shareable {
2  hasNextPage: Boolean!
3  hasPreviousPage: Boolean!
4  startCursor: String
5  endCursor: String
6}