From harness-claude
Implements cursor-based and offset pagination in GraphQL using the Relay connection specification, with encoding and resolver patterns.
How this skill is triggered — by the user, by Claude, or both
Slash command
/harness-claude:graphql-pagination-patternsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Implement cursor-based and offset pagination in GraphQL using the Relay connection specification
Implement cursor-based and offset pagination in GraphQL using the Relay connection specification
Connection/Edge/PageInfo pattern is the industry standard for GraphQL pagination.type Query {
users(first: Int, after: String, last: Int, before: String): UserConnection!
}
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
function encodeCursor(id: string): string {
return Buffer.from(`cursor:${id}`).toString('base64');
}
function decodeCursor(cursor: string): string {
const decoded = Buffer.from(cursor, 'base64').toString('utf-8');
return decoded.replace('cursor:', '');
}
first/after (forward) and last/before (backward) pagination.const resolvers = {
Query: {
users: async (_parent, { first, after, last, before }, { db }) => {
const limit = first ?? last ?? 20;
const afterId = after ? decodeCursor(after) : null;
const beforeId = before ? decodeCursor(before) : null;
const users = await db.users.findPaginated({
limit: limit + 1, // fetch one extra to determine hasNextPage
afterId,
beforeId,
direction: last ? 'backward' : 'forward',
});
const hasMore = users.length > limit;
const nodes = hasMore ? users.slice(0, limit) : users;
if (last) nodes.reverse();
return {
edges: nodes.map((user) => ({
node: user,
cursor: encodeCursor(user.id),
})),
pageInfo: {
hasNextPage: first ? hasMore : false,
hasPreviousPage: last ? hasMore : false,
startCursor: nodes[0] ? encodeCursor(nodes[0].id) : null,
endCursor: nodes[nodes.length - 1] ? encodeCursor(nodes[nodes.length - 1].id) : null,
},
};
},
},
};
Include totalCount when clients need it (e.g., for "showing 1-20 of 342"). Be aware this requires a separate COUNT(*) query, which can be expensive on large tables.
For simple use cases, offset pagination is acceptable. Use it for admin dashboards, data tables, or any context where "jump to page N" is needed and data does not change frequently.
type Query {
users(offset: Int, limit: Int): UserList!
}
type UserList {
items: [User!]!
totalCount: Int!
hasMore: Boolean!
}
fetchMore to load additional pages.const { data, fetchMore } = useQuery(GET_USERS, { variables: { first: 20 } });
const loadMore = () => {
fetchMore({
variables: { after: data.users.pageInfo.endCursor },
updateQuery: (prev, { fetchMoreResult }) => ({
users: {
...fetchMoreResult.users,
edges: [...prev.users.edges, ...fetchMoreResult.users.edges],
},
}),
});
};
first/limit. Default to 20, cap at 100. This prevents clients from requesting unbounded result sets.const limit = Math.min(first ?? 20, 100);
@connection directive (Apollo Client) to give paginated fields a stable cache key when the same field is queried with different pagination arguments.Cursor vs. offset trade-offs:
WHERE id > cursor), no "page drift." Cannot jump to arbitrary pages.OFFSET 10000 scans and discards rows), unstable when items are inserted/deleted between pages.Cursor implementation strategies:
WHERE id > :cursor ORDER BY id — simple, efficient, works when ordering by primary keyWHERE created_at > :cursor ORDER BY created_at — use a composite cursor (timestamp + id) for tiesPerformance considerations:
limit + 1 to determine hasNextPage without a separate count queryWHERE clause must hit an index)totalCount separately if it is expensive and does not need to be real-timeWHERE clause dynamicallyApollo Client cache integration: Apollo's offsetLimitPagination() and relayStylePagination() type policies handle merging paginated results in the cache automatically.
https://relay.dev/graphql/connections.htm
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeImplements Relay's cursor-based GraphQL pagination in React apps using usePaginationFragment for infinite scroll, load more, and automatic cache updates.
Designs cursor-based pagination for live datasets, eliminating page drift. Implements opaque cursors, forward/backward traversal, and stable position encodings.
Guides GraphQL schema design with Relay-style connections, resolver patterns, DataLoader for N+1 prevention, subscriptions, and error handling.