Implementing Pagination

1. Understanding Pagination Patterns

PatternProsCons
OffsetSimple, supports jump-to-pageSlow on large offsets, drift on inserts
Cursor (Relay)Stable, fast, infinite scrollNo jump-to-page
Page-basedFamiliar UXSame drift as offset
KeysetFast, stableCustom client logic

2. Implementing Offset Pagination

Example: Offset args

type Query {
  posts(limit: Int = 20, offset: Int = 0): [Post!]!
  postsCount: Int!
}

3. Implementing Cursor Pagination

ArgumentDirection
first: IntTake N forward
after: StringCursor to start after
last: IntTake N backward
before: StringCursor to end before

4. Using Relay Connection Spec

Example: Relay Connection schema

type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}
type PostEdge { node: Post!, cursor: String! }
type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

5. Creating PageInfo Type

FieldPurpose
hasNextPageMore pages forward
hasPreviousPageMore pages backward
startCursorCursor of first edge
endCursorCursor of last edge

6. Implementing Forward Pagination

Example: Forward pagination resolver

posts: async (_, { first = 20, after }) => {
  const cursor = after ? decode(after) : null;
  const items = await db.post.findMany({
    take: first + 1,
    where: cursor ? { id: { lt: cursor } } : undefined,
    orderBy: { id: "desc" }
  });
  const hasNext = items.length > first;
  const slice = hasNext ? items.slice(0, -1) : items;
  return {
    edges: slice.map(n => ({ node: n, cursor: encode(n.id) })),
    pageInfo: { hasNextPage: hasNext, endCursor: encode(slice.at(-1)?.id) }
  };
}

7. Implementing Backward Pagination

ArgsBehavior
last + beforePage going backward
ValidationReject combo of first+last
OrderReverse query, then re-reverse results

8. Creating Edge Type

FieldUse
nodeThe actual entity
cursorOpaque pointer for resuming
Custom edge fieldse.g., joinedAt for member edges

9. Generating Cursors

MethodDetail
Base64 of idSimple, opaque
Compound keybase64(timestamp:id) for time-ordered lists
SignedHMAC to prevent tampering

10. Handling Empty Results

FieldEmpty Value
edges[]
pageInfo.hasNextPagefalse
startCursor / endCursornull
totalCount0

11. Setting Page Size Limits

Example: Cap page size

const MAX_FIRST = 100;
const take = Math.min(args.first ?? 20, MAX_FIRST);
DefaultMax
20100

12. Implementing Total Count

ConcernDetail
CostSeparate COUNT query (often expensive)
Make optionalOnly resolve when client selects totalCount
Approximate countUse estimated row counts for huge tables