Relay Cursor Connections Specification

Relay’s support for pagination relies on the GraphQL server exposing connections in a standardized way. In the query, the connection model provides a standard mechanism for slicing and paginating the result set. In the response, the connection model provides a standard way of providing cursors, and a way of telling the client when more results are available.

An example of all four of those is the following query:

{
  user {
    id
    name
    friends(first: 10, after: "opaqueCursor") {
      edges {
        cursor
        node {
          id
          name
        }
      }
      pageInfo {
        hasNextPage
      }
    }
  }
}

In this case, friends is a connection. That query demonstrates the four features describe above:

This section of the spec describes the formal requirements around connections.

  1. 1Reserved Types
  2. 2Connection Types
    1. 2.1Fields
      1. 2.1.1Edges
      2. 2.1.2PageInfo
    2. 2.2Introspection
  3. 3Edge Types
    1. 3.1Fields
      1. 3.1.1Node
      2. 3.1.2Cursor
    2. 3.2Introspection
  4. 4Arguments
    1. 4.1Forward pagination arguments
    2. 4.2Backward pagination arguments
    3. 4.3Pagination algorithm
  5. 5PageInfo
    1. 5.1Fields
    2. 5.2Introspection

1Reserved Types

A GraphQL Relay server must reserve certain types and type names to support the pagination model used by Relay. In particular, this spec creates guidelines for the following types:

2Connection Types

Any type whose name ends in “Connection” is considered by Relay to be a Connection Type. Connection types must be an “Object” as defined in the “Type System” section of the GraphQL Specification.

2.1Fields

Connection types must have fields named edges and pageInfo. They may have additional fields related to the connection, as the schema designer sees fit.

2.1.1Edges

A “Connection Type” must contain a field called edges. This field must return a list type that wraps an edge type, where the requirements of an edge type are defined in the “Edge Types” section below.

2.1.2PageInfo

A “Connection Type” must contain a field called pageInfo. This field must return a non‐null PageInfo object, as defined in the “PageInfo” section below.

2.2Introspection

If ExampleConnection existed in the type system, it would be a connection, since its name ends in “Connection”. If this connection’s edge type was named ExampleEdge, then a server that correctly implements the above requirement would accept the following introspection query, and return the provided response:

{
  __type(name: "ExampleConnection") {
    fields {
      name
      type {
        name
        kind
        ofType {
          name
          kind
        }
      }
    }
  }
}

returns

{
  "data": {
    "__type": {
      "fields": [
        // May contain other items
        {
          "name": "pageInfo",
          "type": {
            "name": null,
            "kind": "NON_NULL",
            "ofType": {
              "name": "PageInfo",
              "kind": "OBJECT"
            }
          }
        },
        {
          "name": "edges",
          "type": {
            "name": null,
            "kind": "LIST",
            "ofType": {
              "name": "ExampleEdge",
              "kind": "OBJECT"
            }
          }
        }
      ]
    }
  }
}

3Edge Types

A type that is returned in list form by a connection type’s edges field is considered by Relay to be an Edge Type. Edge types must be an “Object” as defined in the “Type System” section of the GraphQL Specification.

3.1Fields

Edge types must have fields named node and cursor. They may have additional fields related to the edge, as the schema designer sees fit.

3.1.1Node

An “Edge Type” must contain a field called node. This field must return either a Scalar, Enum, Object, Interface, Union, or a Non‐Null wrapper around one of those types. Notably, this field cannot return a list.

The naming echoes that of the “Node” interface and “node” root field as described in a later section of this spec. Relay can perform certain optimizations if this field returns an object that implements Node, however, this is not a strict requirement for use of Relay.

3.1.2Cursor

An “Edge Type” must contain a field called cursor. This field must return a type that serializes as a String; this may be a String, a Non‐Null wrapper around a String, a custom scalar that serializes as a String, or a Non‐Null wrapper around a custom scalar that serializes as a String.

Whatever type this field returns will be referred to as the cursor type in the rest of this spec.

The result of this field is considered opaque by Relay, but will be passed back to the server as described in the “Arguments” section below.

3.2Introspection

If ExampleEdge is an edge type in our schema, that returned “Example” objects, then a server that correctly implements the above requirement would accept the following introspection query, and return the provided response:

{
  __type(name: "ExampleEdge") {
    fields {
      name
      type {
        name
        kind
        ofType {
          name
          kind
        }
      }
    }
  }
}

returns

{
  "data": {
    "__type": {
      "fields": [
        // May contain other items
        {
          "name": "node",
          "type": {
            "name": "Example",
            "kind": "OBJECT",
            "ofType": null
          }
        },
        {
          "name": "cursor",
          "type": {
            // This shows the cursor type as String!, other types are possible
            "name": null,
            "kind": "NON_NULL",
            "ofType": {
              "name": "String",
              "kind": "SCALAR"
            }
          }
        }
      ]
    }
  }
}

4Arguments

A field that returns a Connection Type must include forward pagination arguments, backward pagination arguments, or both. These pagination arguments allow the client to slice the set of edges before it is returned.

4.1Forward pagination arguments

To enable forward pagination, two arguments are required.

The server should use those two arguments to modify the edges returned by the connection, returning edges after the after cursor, and returning at most first edges.

4.2Backward pagination arguments

To enable backward pagination, two arguments are required.

The server should use those two arguments to modify the edges returned by the connection, returning edges before the before cursor, and returning at most last edges.

4.3Pagination algorithm

To determine what edges to return, the connection evaluates the before and after cursors to filter the edges, then evaluates first to slice the edges, then last to slice the edges.

Including a value for both first and last is strongly discouraged, as it is likely to lead to confusing queries and results. The PageInfo section goes into more detail here.

More formally:

EdgesToReturn(allEdges, before, after, first, last)
  1. Let edges be the result of calling ApplyCursorsToEdges(allEdges, before, after).
  2. If first is set:
    1. If first is less than 0:
      1. Throw an error.
    2. If edges has length greater than than first:
      1. Slice edges to be of length first by removing edges from the end of edges.
  3. If last is set:
    1. If last is less than 0:
      1. Throw an error.
    2. If edges has length greater than than last:
      1. Slice edges to be of length last by removing edges from the start of edges.
  4. Return edges.
ApplyCursorsToEdges(allEdges, before, after)
  1. Initialize edges to be allEdges.
  2. If after is set:
    1. Let afterEdge be the edge in edges whose cursor is equal to the after argument.
    2. If afterEdge exists:
      1. Remove all elements of edges before and including afterEdge.
  3. If before is set:
    1. Let beforeEdge be the edge in edges whose cursor is equal to the before argument.
    2. If beforeEdge exists:
      1. Remove all elements of edges after and including beforeEdge.
  4. Return edges.

5PageInfo

The server must provide a type called PageInfo.

5.1Fields

PageInfo must contain fields hasPreviousPage and hasNextPage, both of which return non‐null booleans.

hasPreviousPage will be false if the client is not paginating with last, or if the client is paginating with last, and the server has determined that the client has reached the end of the set of edges defined by their cursors. More formally:

HasPreviousPage(allEdges, before, after, first, last)
  1. If last was not set, return false.
  2. Let edges be the result of calling ApplyCursorsToEdges(allEdges, before, after).
  3. If edges contains more than last elements, return true.
  4. Return false.

hasNextPage will be false if the client is not paginating with first, or if the client is paginating with first, and the server has determined that the client has reached the end of the set of edges defined by their cursors. More formally:

HasNextPage(allEdges, before, after, first, last)
  1. If first was not set, return false.
  2. Let edges be the result of calling ApplyCursorsToEdges(allEdges, before, after).
  3. If edges contains more than first elements, return true.
  4. Return false.
hasPreviousPage is only meaningful when last is included, as it is always false otherwise. hasNextPage is only meaningful when first is included, as it is always false otherwise. When both first and last are included, both of the fields are set according to the above algorithms, but their meaning as it relates to pagination becomes unclear. This is among the reasons that pagination with both first and last is discouraged.

5.2Introspection

A server that correctly implements the above requirement would accept the following introspection query, and return the provided response:

{
  __type(name: "PageInfo") {
    fields {
      name
      type {
        name
        kind
        ofType {
          name
          kind
        }
      }
    }
  }
}

returns

{
  "data": {
    "__type": {
      "fields": [
        // May contain other fields.
        {
          "name": "hasNextPage",
          "type": {
            "name": null,
            "kind": "NON_NULL",
            "ofType": {
              "name": "Boolean",
              "kind": "SCALAR"
            }
          }
        },
        {
          "name": "hasPreviousPage",
          "type": {
            "name": null,
            "kind": "NON_NULL",
            "ofType": {
              "name": "Boolean",
              "kind": "SCALAR"
            }
          }
        }
      ]
    }
  }
}