Implementing Pagination
1. Understanding Pagination Patterns
| Pattern | Pros | Cons |
|---|---|---|
| Offset | Simple, supports jump-to-page | Slow on large offsets, drift on inserts |
| Cursor (Relay) | Stable, fast, infinite scroll | No jump-to-page |
| Page-based | Familiar UX | Same drift as offset |
| Keyset | Fast, stable | Custom 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
| Argument | Direction |
|---|---|
| first: Int | Take N forward |
| after: String | Cursor to start after |
| last: Int | Take N backward |
| before: String | Cursor 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
| Field | Purpose |
|---|---|
| hasNextPage | More pages forward |
| hasPreviousPage | More pages backward |
| startCursor | Cursor of first edge |
| endCursor | Cursor 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
| Args | Behavior |
|---|---|
| last + before | Page going backward |
| Validation | Reject combo of first+last |
| Order | Reverse query, then re-reverse results |
8. Creating Edge Type
| Field | Use |
|---|---|
| node | The actual entity |
| cursor | Opaque pointer for resuming |
| Custom edge fields | e.g., joinedAt for member edges |
9. Generating Cursors
| Method | Detail |
|---|---|
| Base64 of id | Simple, opaque |
| Compound key | base64(timestamp:id) for time-ordered lists |
| Signed | HMAC to prevent tampering |
10. Handling Empty Results
| Field | Empty Value |
|---|---|
| edges | [] |
| pageInfo.hasNextPage | false |
| startCursor / endCursor | null |
| totalCount | 0 |
11. Setting Page Size Limits
| Default | Max |
|---|---|
| 20 | 100 |
12. Implementing Total Count
| Concern | Detail |
|---|---|
| Cost | Separate COUNT query (often expensive) |
| Make optional | Only resolve when client selects totalCount |
| Approximate count | Use estimated row counts for huge tables |