Two strategies: offset-based (limit/offset) and cursor-based. GraphQL has no built-in pagination, but the community standard is the Relay Connection spec, which formalizes cursor pagination.
Two strategies: offset-based (limit/offset) and cursor-based. GraphQL has no built-in pagination, but the community standard is the Relay Connection spec, which formalizes cursor pagination.
items(limit: 10, offset: 20)) is simple but breaks on live data: if a row is inserted while the user pages, items shift and you see duplicates or gaps. It also gets slow at deep offsets (the DB still scans skipped rows).A connection wraps the list in edges (each with a cursor and node) plus pageInfo:
type Query {
posts(first: Int!, after: String): PostConnection! # first = page size, after = cursor
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
cursor: String! # opaque token for THIS node's position
node: Post! # the actual item
}
type PageInfo {
endCursor: String # feed this back as 'after' for the next page
hasNextPage: Boolean! # so the client knows when to stop
}
// resolver: fetch ONE extra row to compute hasNextPage cheaply
const rows = await db.posts.find({ after: args.after, limit: args.first + 1 });
const hasNextPage = rows.length > args.first;
const page = rows.slice(0, args.first);
return {
edges: page.map((p) => ({ node: p, cursor: encodeCursor(p.id) })),
pageInfo: { endCursor: page.at(-1) && encodeCursor(page.at(-1).id), hasNextPage },
};
The cursor is opaque — usually a base64 of the sort key — so clients treat it as a token, not a number.
Choosing offset vs cursor shows you understand consistency under concurrent writes and deep-page performance. Knowing the Relay connection shape matters because Apollo/Relay clients have built-in cache handling for it.
A library of IT interview questions with detailed answers — from Junior to Senior.
Donate