Designing Pagination-Friendly Schemas
Large collections should not normally be returned without bounds.
A naive schema might expose:
users: [User!]!
This becomes dangerous if the database contains millions of users.
A basic pagination design could use:
users(limit: Int!, offset: Int!): [User!]!
But large-scale APIs often use cursor-based pagination because cursors can provide more stable traversal semantics when data changes during pagination.
A connection-style design can expose metadata:
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}
The client can request:
users(first: 20, after: $cursor) {
edges {
node {
id
name
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
This structure is more verbose than returning a raw list, but it provides room for pagination metadata and future evolution.
Pagination strategy should match the underlying data store and access patterns. Cursor pagination is not a magical performance solution by itself; the cursor design and database query still matter.
limit + offsetafter + cursoredges + pageInfo- Avoid unbounded collection fields in production APIs.
- Pagination should be designed into collection operations.
- Cursor-based pagination can provide stable traversal semantics.
- Connection types expose useful metadata around paginated results.
Why is returning an unbounded list such as `users: [User!]!` risky in a production API?