StepZen is now part of IBM. For the most recent product information and updates go to
https://www.ibm.com/products/stepzen

GraphQL Pagination and REST Services

How to configure the @rest directive to implement GraphQL pagination

Sometimes backends return a lot of information (e.g. pages and pages of previous orders), but your end users might not need to consume all of that information.

This can be handled by paginating the results so you can limit the number of results returned, while maintaining the ability to obtain more results in subsequent requests.

GraphQL specifies cursor-based pagination as the best practice for paginating through data to traverse relationships between sets of objects in a GraphQL API. (A cursor is a type of pointer to the last item in the set of data, which is sent to the client so that the server can return the results after it.)

That is, cursor pagination returns a specified number of results per request, relative to a cursor in the result set managed by the backend API.

GraphQL uses nodes and edges to implement cursors. A node is a group of data. An edge represents the connection between two nodes, and consists of the edge object, the underlying node onject, and the cursor.

Configuring Pagination

StepZen allows you to use three different methods of pagination, based on what's supported by your backend API:

You enable and configure pagination in StepZen by adding pagination to your @rest directive and configuring its two subfields:

  • type: Specifies the type of pagination to use. Can be set to:

    • PAGE_NUMBER: Returns a page of results corresponding to a certain one-based page number. In each request you must specify the same number of results to return per page.
    • OFFSET: Returns results from a zero-based index into the result set. In each request you can specify the number of results to return.
    • NEXT_CURSOR: Returns a set of results from a cursor location managed by the backend API. In each request you can specify the number of results to return.
  • setters: Specifies the total number of results/pages. Can be set to one of the following based on the pagination type:

    TypeSetters
    PAGE_NUMBER[{field:"total" path: "meta.total_pages"}]
    OFFSET[{field:"total" path: "meta.total_count"}]
    NEXT_CURSOR[{field:"nextCursor" path: "meta.next"}]

You then add arguments to the endpoint to specify the starting page/result and number of results to return. These arguments vary depending on the pagination type:

TypeStarting Page/ResultNumber of Results to Return
PAGE_NUMBERpage=$afterper_page=$first
OFFSEToffset=$afterlimit=$first
NEXT_CURSORoffset=$afterlimit=$first

The following example shows a pagination object within an @rest directive:

type User {
    id: ID!
    email: String!
    ...
  }

type UserEdge {
  node: User
  cursor: String
}

type UserConnection {
  pageInfo: PageInfo!
  edges: [UserEdge]
}

  ...

type Query {
  user(id: ID!): User
    @rest(endpoint: "https://reqres.in/api/users/$id", resultroot: "data")
  users(first: Int! = 6, after: String! = ""): UserConnection
    @rest(
      endpoint: "https://reqres.in/api/users?page=$after&per_page=$first"
      resultroot: "data[]"
      pagination: {
        type: PAGE_NUMBER
        setters: [{ field: "total", path: "total_pages" }]
      }
    )
}

Implementing Pagination

Whether functionality is cursor-based, offset, or by page number, it is implemented using the following types.

type Customer {
  activities: [Activity]
  addresses: [Address]
  contacts: Contacts
  description: String
  designation: String
}

type CustomerEdge {
  node: Customer
  cursor: String
}

type CustomerConnection {
  pageInfo: PageInfo!
  edges: [CustomerEdge]
}

The type CustomerEdge takes the initial type Customer as its node. Then, type CustomerConnection takes type CustomerEdge as its edge field value. You might wonder what role the pageInfo field is playing here. The PageInfo value provided to it is returned by the server with information to assist with pagination.

That means that a query like the following will return data to help the user paginate the API.

query MyQuery {
  customers {
    pageInfo {
      endCursor
      hasNextPage
      hasPreviousPage
      startCursor
    }
  }
}

In this example, the data returned is under the customers object.

{
  "data": {
    "customers": {
      "pageInfo": {
        "endCursor": "eyJjIjoiTzpRdWVyeTpwYXJrcyIsIm8iOjE5fQ==",
        "hasNextPage": true,
        "hasPreviousPage": false,
        "startCursor": ""
      }
    }
  }
}

The following subsections describe how to work with each type of pagination within queries.

Page Number Pagination

Page number pagination returns one page of results for each request. The same number of results are returned each time (unless the number of results left to return is less than the number requested).

The example below shows how to use page number pagination:

customers(first:Int! =20 after:String! =""): CustomerConnection
  @rest(
    endpoint:"https://api.example.com/customers?page=$after&per_page=$first"
    resultroot:"data[]"
    pagination: {
        type: PAGE_NUMBER
        setters: [{field:"total" path: "meta.total_pages"}]
      }
    )

The following key aspects are shown in the example:

  • after is set to an empty string or null for the first request. This indicates that it's the first request for results.
  • The initial value for first is set to 20 to indicate that 20 edges (results) are to be returned per page. first must be set to the same value in subsequent requests.

The following must be set in subsequent requests:

  • The value for first must be the same from the initial request (20 in the example above).
  • after must be set to the next page of results to return (e.g. 2 for page two). To get the next page of results, set the value to connection.pageInfo.endCursor from the previous request.
  • The virtual field total must be set to the value of pagination.setters from the previous response. This specifies the total number of groups in the result set.

Note: The opaque cursor after argument is unpacked to contain the backend API service's page number integer value when used in the context of @rest (e.g. as $after in endpoint). The page number is one based, so the first edge in the paged set will be from page 1.

Offset Pagination

Offset pagination returns a specified number of results per request, relative to a zero-based index from the start of the result set.

The example below shows how to use offset pagination:

customers(first:Int! =20 after:String! =""): CustomerConnection
  @rest(
    endpoint:"https://api.example.com/customers?limit=$first&offset=$after"
    resultroot:"data[]"
    pagination: {
        type: OFFSET
        setters: [{field:"total" path: "meta.total_count"}]
      }
    )

The following key aspects are shown in the example:

  • after is set to an empty string or null for the first request. This indicates that it's the first request for results.

The following must be set in subsequent requests:

  • The value for first can be any number of results to return.
  • The value for after is set to the opaque cursor value of connection.pageInfo.endCursor of the previous request to get the next group of results.
  • The virtual field total must be set to the value of pagination.setters from the previous response. This specifies the total number of groups in the result set.

Note: The opaque cursor after argument is unpacked to contain the backend API service's page number integer value when used in the context of @rest (e.g. as $after in endpoint). The offset is zero-based, so the first edge in the paged set has offset zero.

Cursor Pagination

Here is an example of a GraphQL query that uses an edge.

query MyQuery {
  customers(first: 3, after: "eyJjIjoiTzpRdWVyeTpwYXJrcyIsIm8iOjl9") {
    edges {
      node {
        id
        customerCode
      }
    }
  }
}

The first argument specifies that only the first three API responses should be returned. The after argument specifies a starting cursor position. So the above GraphQL query returns the first 3 responses after the cursor "eyJjIjoiTzpRdWVyeTpwYXJrcyIsIm8iOjl9".