In addition to fetching a single document, we need to be able to fetch a list of documents: a collection. In simplest classes, a collection will never contain many elements: for example, professors for a given department. However, when collections can become arbitrarily large, for example with mail messages in a mailbox, we need to be able to specify which elements we are interested in. In this case, we will use pagination, sorting and optionally filtering.
To provide pagination, two main approaches are followed:
- Offset-based pagination - A fixed position, or offset, is given to the query of the collection.
- Cursor-based pagination - An identifier for the first item, or cursor, is given to the query of the collection. This strategy is more popular nowadays, because the cursor is opaque and promotes evolution of the API.
When using pagination, a sorting mechanism should be used. Typically, our data will be backed in a datastore, like a database, with indices and keys. A public id can be used to paginate, but other options can be provided. For example, we can let our users sort our collection by a date field.
As it happened with simple resources, or documents, to fetch a collection resource GET
can be used.
The two main strategies can be used:
- Offset-based pagination - as in
GET /universities?limit=10&offset=30
. - Cursor-based pagination - as in
GET /universities?limit=10&next=uc3m
.
The returned resultset can also contain Web Links
headers to help traversing the rest of the collection.
To sort the results, we can use query parameters as well: GET /universities?limit=10&offset=20&sort-by=establishment-date
.
To filter the resultset, query parameters can be used as well. This technique includes basic filtering, like this:
GET /universities?country=spain
But also more advanced filters, making use of operands, for example using something like the following:
GET /universities?country[neq]=spain
- Cursor-based pagination in Facebook API
- Pagination with Web Links in GitHub API
- OData Querying Collections
GraphQL gives total freedom to implementers on how they design their queries to collections.
With regard to pagination, both approaches, offset-based and cursor-based, can be used. However, most people, even the Official GraphQL documentation, recommend following a specific pattern: the GraphQL Cursor Connections Specification. The collection will return an object like this:
{
"pageInfo": {
"hasNextPage": "<boolean>",
"hasPreviousPage": "<boolean>",
"startCursor": "<opaque cursor to the first one>",
"endCursor": "<opaque cursor to the last one>"
},
"edges": [
{
"cursor": "<opaque cursor of this node>",
"node": "<object with this node representation>",
},
{
"cursor": "<opaque cursor of this node>",
"node": "<object with this node representation>",
}
]
}
And the collection query would accept several arguments. In case of forward-pagination:
first
: non-negative numberafter
: cursor
And in case of backward-pagination:
last
: non-negative numberbefore
: cursor
As in:
type ProductEdge {
cursor: String!
node: Product!
}
type ProductsPayload {
edges: [ProductEdge]!
pageInfo: PageInfo!
totalCount: Int!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type Query {
products(after: String, first: Int, before: String, last: Int): ProductsPayload
}
Note that, in addition to the fields required by the Connections Specification, a totalCount
might be added.
The Connections Specification does not state anything about how to order the results. So a query might have a predefined sorting field, but also allow to specify another field, for example via an optional argument.
GraphQL queries accept arguments. These can be used also to filter arguments. This filtering can be basic, as in:
query {
products(type: FURNITURE) {
totalCount
edges {
node {
name
price(currency: EURO)
}
}
}
}
More advanced patterns can be also used, as in:
query {
products(
where: {
price: {
_gt: 200
}
}
) {
totalCount
}
}
- Relay, the JS framework for react app powered by GraphQL, uses the Connection Specification.
- Hasura: Filter query results, the engine that easily provides a GraphQL server from existing databases, allows the use of advance search operators.
Google Cloud API Design Guide recommends that every List operation contains cursor-based pagination and sorting order from the beginning-
service Blog {
rpc ListArticle(ListArticlesRequest)
returns (ListArticlesResponse);
}
message ListArticlesRequest {
int32 page_size = 1;
string page_token = 2;
string order_by = 3;
}
message ListArticlesResponse {
repeated Article articles = 1;
string next_page_token = 2;
}
The order_by
will be like the SQL ORDER BY
, so that it accepts several fields and sorting criteria, as in id, name desc
.
To perform a filtering, gRPC suggest we use a custom method Search
instead of List
. In this case, a custom implementation will be done. Some Google services use a query
string argument that accepts its own query definition, as in field_name=literal_string
.
The blog-like repository in the sample code supports traversing the articles collection:
In this example, 100 articles have been created. To fetch them, run:
curl http://localhost:4000/articles
Note that only 10 have been returned. A default limit of 10 has been set. To set another limit, use:
curl http://localhost:4000/articles?limit=20
The default offset is 0. To specify it (note the double quotes):
curl "http://localhost:4000/articles?limit=20&offset=30"
To see the WebLinks
generated by this example, see the Link
header after running:
curl -v "http://localhost:4000/articles?limit=20&offset=30"
In this example, 100 articles have been created. To fetch them, run:
query {
articles {
edges {
node {
title
}
}
}
}
As in:
curl -H "Content-Type: application/json" \
-d '{ "query": "query { articles { edges { node { title } } } }" }' \
http://localhost:4000/graphql
Note that only 10 have been returned. A default limit of 10 has been set. To set another limit, use:
query {
articles(first: 20) {
edges {
cursor
node {
title
}
}
}
}
Now, note we have now included cursors in our request. These will be required to paginate throughout the resultset. The default cursor is the first item. To specify another, use (replacing the cursor id):
query {
articles(first: 20, after: "5fa80c365fcbc87a96a7ebc5") {
edges {
cursor
node {
title
}
}
}
}
To see more navigational info about our cursor, we can fetch the totalCount
and the pageInfo
document:
query {
articles(first: 20, after: "5fa80c365fcbc87a96a7ebc5") {
totalCount
pageInfo {
endCursor
hasNextPage
}
edges {
cursor
node {
title
}
}
}
}
A new rpc
has been created. It accepts a request object to configure the pagination, and returns a response object that contains the next cursor, as well as a list of articles:
service Main {
rpc ListArticles(ListArticlesRequest)
returns (ListArticlesResponse);
}
message ListArticlesRequest {
int32 page_size = 1;
optional string page_token = 2;
}
message ListArticlesResponse {
message BasicArticle {
string id = 1;
string title = 2;
string description = 3;
}
repeated BasicArticle articles = 1;
string next_page_token = 2;
}
To execute it, run the gprc client, npm run grpcc
, and then:
client.ListArticles({}, pr)
Then, to get from a specific page, set a valid article id, as in:
client.ListArticles({page_token:'5fc3ffe378b3dd2565ed83f3'}, pr)