patterntypescriptgraphqlModerate
GraphQL pagination with Relay Connections — the edges/node/cursor pattern
Viewed 0 times
paginationrelay connectionscursoredgespageInfohasNextPageconnection spec
Problem
Offset-based pagination breaks when items are inserted or deleted during pagination. Simple arrays of items don't carry metadata (hasNextPage, total count). Different pagination implementations per field make client code inconsistent.
Solution
Use the Relay Connection specification. Define Connection and Edge types per paginated field.
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type Query {
users(first: Int, after: String, last: Int, before: String): UserConnection!
}// Resolver using prisma cursor pagination
const Query = {
users: async (_root, { first = 10, after }) => {
const items = await db.user.findMany({
take: first + 1,
cursor: after ? { id: decodeCursor(after) } : undefined,
});
const hasNextPage = items.length > first;
const nodes = items.slice(0, first);
return {
edges: nodes.map(n => ({ node: n, cursor: encodeCursor(n.id) })),
pageInfo: { hasNextPage, endCursor: encodeCursor(nodes.at(-1)?.id) },
totalCount: await db.user.count(),
};
},
};Why
Cursor pagination is stable when data changes between pages. The Connection spec is a well-known convention that tools, code generators, and clients like Relay and Apollo understand natively.
Gotchas
- Requesting totalCount on every page is expensive — make it optional or compute separately
- Base64-encode cursors to make them opaque to clients — clients should not parse them
- Apollo Client's
relayStylePagination()helper handles field merging for connection fields - Combining cursor pagination with arbitrary sorting requires including sort fields in the cursor
Context
Any GraphQL field returning a potentially large list of items
Revisions (0)
No revisions yet.