GraphQL Pagination and REST Services
Paginate a REST API as a data source using the @rest directive.
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:Type Setters 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:
| Type | Starting Page/Result | Number of Results to Return |
|---|---|---|
| PAGE_NUMBER | page=$after |
per_page=$first |
| OFFSET | offset=$after |
limit=$first |
| NEXT_CURSOR | offset=$after |
limit=$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 following example 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:
afteris set to an empty string ornullfor the first request. This indicates that it's the first request for results.- The initial value for
firstis set to20to indicate that 20 edges (results) are to be returned per page.firstmust be set to the same value in subsequent requests.
The following must be set in subsequent requests:
- The value for
firstmust be the same from the initial request (20 in the preceding example). aftermust be set to the next page of results to return (e.g.2for page two). To get the next page of results, set the value toconnection.pageInfo.endCursorfrom the previous request.- The virtual field total must be set to the value of
pagination.settersfrom 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 following example 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:
afteris set to an empty string ornullfor the first request. This indicates that it's the first request for results.
The following must be set in subsequent requests:
- The value for
firstcan be any number of results to return. - The value for
afteris set to the opaque cursor value ofconnection.pageInfo.endCursorof the previous request to get the next group of results. - The virtual field total must be set to the value of
pagination.settersfrom 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 preceding GraphQL query returns the first 3 responses after the cursor "eyJjIjoiTzpRdWVyeTpwYXJrcyIsIm8iOjl9".