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:
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
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 theafter
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
andEdge
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
andCustomer
:GraphQL1type 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 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 businessIN_STORE
and anotherONLINE
. Thetype
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
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}
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
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}
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
1query MyPosts($page: Int) {
2 viewer {
3 posts(page: $page) {
4 nodes {
5 id
6 title
7 content
8 }
9 totalPages
10 }
11 }
12}
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:
1type PageInfo @shareable {
2 hasNextPage: Boolean!
3 hasPreviousPage: Boolean!
4 startCursor: String
5 endCursor: String
6}