Pagination is an essential feature in APIs, particularly when dealing with large datasets. It allows users to retrieve data in smaller, manageable chunks instead of overwhelming the server or client with large responses. In this chapter, we’ll explore GraphQL pagination and cursor-based pagination techniques in Express.js, from basic to advanced concepts. We'll cover the A to Z of pagination, including implementation details, examples, and explanations.
When working with databases or APIs that return large datasets, sending all data at once can lead to performance issues, longer load times, and high memory usage. Pagination addresses these concerns by splitting the data into smaller, more manageable parts, allowing clients to request only the data they need.
limit
and skip
to return a subset of data. For example, limit=10&offset=20
returns the 10 items starting from the 20th.In this chapter, we will focus on Cursor-Based Pagination, which is highly recommended for GraphQL APIs.
In a GraphQL API, pagination allows you to retrieve a limited subset of data in a single query. This is typically done using connections and edges. The connection is a wrapper around a list of items, while the edge contains the node (item) itself and additional pagination information, such as cursors.
GraphQL uses two primary methods for pagination:
In cursor-based pagination, each record in the dataset has a unique cursor that acts as a pointer. This cursor can be used to fetch the next or previous set of results in subsequent requests. Instead of using skip
and limit
like offset-based pagination, cursor-based pagination uses the before
and after
arguments along with the cursor to fetch the next or previous page of data.
Before diving into pagination, we need to set up a simple Express.js server with GraphQL. This setup includes creating a GraphQL schema and resolvers.
npm install express graphql express-graphql
const express = require("express");
const { graphqlHTTP } = require("express-graphql");
const { GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLList } = require("graphql");
const app = express();
// Define the GraphQL schema
const ItemType = new GraphQLObjectType({
name: "Item",
fields: {
id: { type: GraphQLString },
name: { type: GraphQLString },
},
});
const QueryType = new GraphQLObjectType({
name: "Query",
fields: {
items: {
type: new GraphQLList(ItemType),
resolve: () => [
{ id: "1", name: "Item 1" },
{ id: "2", name: "Item 2" },
{ id: "3", name: "Item 3" },
],
},
},
});
const schema = new GraphQLSchema({
query: QueryType,
});
// Set up the Express server with GraphQL endpoint
app.use("/graphql", graphqlHTTP({
schema,
graphiql: true,
}));
app.listen(4000, () => {
console.log("Server running on http://localhost:4000/graphql");
});
This basic setup defines a simple schema with an items
query returning a list of items.
We’ll now modify the schema to implement cursor-based pagination. We will introduce edges
and pageInfo
to the schema.
const { GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLList, GraphQLInt } = require("graphql");
const { connectionArgs, connectionDefinitions, connectionFromArraySlice } = require("graphql-relay");
const { ItemConnection } = connectionDefinitions({
nodeType: ItemType,
});
const PageInfoType = new GraphQLObjectType({
name: "PageInfo",
fields: {
hasNextPage: { type: GraphQLString },
hasPreviousPage: { type: GraphQLString },
},
});
const QueryType = new GraphQLObjectType({
name: "Query",
fields: {
items: {
type: ItemConnection,
args: {
...connectionArgs, // pagination arguments (first, after, last, before)
},
resolve: (parent, args) => {
const items = [
{ id: "1", name: "Item 1" },
{ id: "2", name: "Item 2" },
{ id: "3", name: "Item 3" },
{ id: "4", name: "Item 4" },
{ id: "5", name: "Item 5" },
];
// Using connectionFromArraySlice to slice items and manage pagination
return connectionFromArraySlice(items, args, {
sliceStart: 0,
arrayLength: items.length,
});
},
},
},
});
const schema = new GraphQLSchema({
query: QueryType,
});
connectionArgs
: These arguments include first
, after
, last
, and before
, which are used for pagination.connectionFromArraySlice
: This function handles slicing the data array based on the pagination arguments.Here’s how a client would query the data with pagination:
query {
items(first: 2) {
edges {
node {
id
name
}
}
pageInfo {
hasNextPage
}
}
}
first: 2
: Fetch the first 2 items.edges
: Contains the paginated results (nodes).pageInfo
: Contains metadata about the pagination (e.g., hasNextPage
).To handle cursors, you need to encode and decode them. Cursors are typically base64-encoded strings representing unique identifiers for records.
const encodeCursor = (itemId) => Buffer.from(itemId).toString("base64");
const decodeCursor = (cursor) => Buffer.from(cursor, "base64").toString("utf-8");
// Example usage:
const cursor = encodeCursor("3"); // Encoding the item ID '3'
console.log(decodeCursor(cursor)); // Decodes back to '3'
In the schema, you would pass these cursors as after
and before
arguments to fetch the next/previous page.
In cursor-based pagination, cursors are tied to specific records, so when data is sorted by a field (e.g., name or date), the cursor reflects the sorting order. This ensures that pagination is consistent even as data changes.
pageInfo
to help clients understand the pagination state.In this chapter, we explored GraphQL pagination and cursor-based pagination in Express.js. We discussed how to set up basic and advanced pagination using GraphQL schema, how cursors work, and how to implement efficient pagination strategies for large datasets. With the examples and explanations provided, you should now have a solid understanding of how to implement GraphQL pagination in your Express.js applications. Happy coding !❤️