Introduction
Modern applications rely heavily on dynamic data feeds — user lists, product catalogs, and activity streams. To make these performant, sorting and pagination in GraphQlRepository are essential. Unlike REST APIs, which depend on query parameters (?limit=10&page=2
), GraphQL allows you to define structured arguments directly in your schema.
With the right design, you can deliver lightweight responses, reduce bandwidth usage, and give your clients full control over data ordering and navigation. By the end of this guide, you’ll master how to design schemas, implement resolvers, and apply best practices for paginated and sorted responses in GraphQL.
Why Sorting and Pagination Matter
When datasets grow into thousands or millions of records, returning everything at once is impractical. Pagination divides large datasets into manageable “pages,” while sorting determines the order records appear in.
Common use cases
- Displaying user lists in admin dashboards
- Social feeds or comment threads
- Search results with sorting by relevance or date
Efficient pagination reduces database load, and combining it with sorting ensures users always see consistent, meaningful data.
Schema Design for Sorting and Pagination
Designing a schema is the first step to enabling robust sorting of paginated results.
Sorting Input Types
Start with defining a reusable enum for order direction and an input type that specifies sort fields:
enum SortOrder {
ASC
DESC
}
input UserOrderByInput {
name: SortOrder
age: SortOrder
createdAt: SortOrder
}
This structure enables clients to request complex sorting such as orderBy: { createdAt: DESC, name: ASC }
without changing your API code.
Pagination Arguments
Typical pagination arguments in a GraphQL schema include:
- Offset‑based pagination: uses
first
(limit) andoffset
(skip) - Cursor‑based pagination: uses
first
,after
,before
- Relay‑style connections: follows
edges
andpageInfo
for consistency with Relay clients
Schema Design Tips
- Use enums for clarity and built‑in validation.
- Support compound sorting to give clients flexibility.
- Always pair sorting arguments with pagination fields.
Implementing Resolver Logic
Your resolver should accept sorting and pagination arguments and translate them into efficient database queries.
Core Responsibilities
- Filtering — narrow the dataset.
- Sorting — apply
orderBy
in the specified direction. - Counting — compute total results for UI pagination.
- Slicing — fetch results corresponding to
first
andoffset
or cursor bounds. - Returning data — include metadata such as total count and page information.
Example Flow (Pseudocode)
async function usersResolver(args) {
let records = await db.Users.filter(args.filter);
records = await records.sort(args.orderBy);
const total = await records.count();
const sliced = records.slice(args.offset, args.offset + args.first);
return { data: sliced, totalCount: total };
}
Database Integration
Most GraphQL resolvers delegate to an ORM (e.g., Prisma, TypeORM, or Sequelize) that supports ORDER BY, LIMIT, and OFFSET. Index frequently sorted fields such as createdAt
or updatedAt
to avoid full table scans.
Example with Prisma ORM:
const users = await prisma.user.findMany({
take: 10,
skip: 20,
orderBy: { createdAt: 'desc' },
});
Pagination Techniques Explained
Technique | Description | Pros | Cons |
---|---|---|---|
Offset‑based | Uses first and offset to control result range. | Easy to understand, widely supported. | Slow on large tables; results can shift if data changes. |
Cursor‑based | Uses after /before and unique cursors. | Excellent for large datasets; stable ordering. | Requires more complex implementation. |
Relay‑style | Builds on cursor pagination with edges and pageInfo . | Standardized for React/Relay clients. | Verbose schema structure. |
Cursor-based pagination is generally best for continuously updating datasets (feeds or messages), as it avoids skipping records when data changes.
To ensure stability, always generate cursors from unique and sequential fields (e.g., createdAt + id
) to avoid duplicate or missing records.
Example GraphQL Queries
Offset‑Based Query
query {
users(
orderBy: { createdAt: DESC }
first: 10
offset: 20
) {
name
age
}
}
This retrieves users sorted by createdAt
, skipping the first 20 and returning 10 more.
Cursor‑Based Query
query {
users(
orderBy: { createdAt: DESC }
first: 10
after: "cursorValue"
) {
edges {
node {
name
age
}
cursor
}
pageInfo {
hasNextPage
}
}
}
Here, cursorValue
is often a base64‑encoded combination of sorting keys like createdAt
and id
.
Best Practices for Sorting Paginated Results
- Sort on indexed fields to prevent slow queries.
- Limit query size using
first
orlimit
. - Filter before sorting to minimize data volume.
- Use descriptive enums for
ASC
/DESC
to improve API usability. - Allow compound sorting — users may need
createdAt DESC, name ASC
. - Include total count to support UI pagination controls.
- Document exactly which pagination method your API supports.
A combination of effective indexing, predictable cursors, and documentation ensures both client performance and developer friendliness.
Framework Focus: Spring Data GraphQL (@GraphQlRepository
)
For developers using Spring Boot, Spring Data GraphQL drastically simplifies pagination and sorting. It bridges GraphQL queries and standard Spring Data repositories.
Repository Integration
You can annotate your repository:
@GraphQlRepository
public interface UserRepository extends PagingAndSortingRepository<User, Long> {}
This enables automatic translation between GraphQL query arguments (page
, size
, sort
) and JPA’s paging abstractions.
Example GraphQL Query
query {
users(page: { number: 0, size: 10 }, sort: [{ field: "name", direction: ASC }]) {
content {
name
age
}
totalElements
}
}
Spring Data GraphQL converts this to an optimized JPA query with ORDER BY name ASC
and proper pagination limits.
Advantages
- Eliminates manual resolver code.
- Relies on familiar
Pageable
andSort
abstractions. - Automatically returns metadata such as total elements.
Note: @GraphQlRepository
is part of the Spring GraphQL extensions and its behavior may vary slightly depending on the Spring Boot and Spring GraphQL versions. Always confirm with the Spring GraphQL documentation.
Implementation Checklist
- Define
SortOrder
enums and sorting input types. - Add pagination arguments (
first
,offset
,after
,before
). - Implement resolver logic or integrate with a framework like Spring Data.
- Optimize database tables with indexes on frequently sorted fields.
- Return total counts for UIs.
- Test with both static and dynamically changing datasets.
Summary Table
Step | Offset‑based | Cursor‑based |
---|---|---|
Schema | first , offset , orderBy | first , after , orderBy , edges |
Resolver | Slice after sorting | Use cursor from sort key |
Client Behavior | Increment offset | Move forward using cursor |
Performance | Slower with large offsets | Stable on large datasets |
UI Integration | Page numbers | Infinite scroll / “Load more” |
Conclusion
Building performant GraphQL APIs means mastering sorted pagination. Always remember the key sequence — filter → sort → paginate.
Offset‑based pagination suits small, static datasets, while cursor‑based pagination offers stability and scalability for large or frequently changing data. Frameworks like Spring Data GraphQL automate much of this process, letting you focus on delivering well‑structured data instead of query handling.
With thoughtful schema design, resolver optimization, and clear documentation, your GraphQL API can efficiently handle millions of records while delivering a seamless user experience.
FAQs on Sorting Paginated Results in GraphQL
Q1. Should I use offset‑based or cursor‑based pagination?
Cursor‑based pagination is more efficient for large or dynamic datasets as it avoids scanning skipped rows and remains stable when new records are inserted.
Q2. What if I sort on a non‑indexed field?
The database performs a full table scan, drastically reducing query performance. Always index fields used for sorting like createdAt
or name
.
Q3. Can I mix filtering, sorting, and pagination?
Yes. The best approach is to filter → sort → paginate sequentially for consistent output and optimized queries.
Q4. What’s the benefit of returning total count in GraphQL?
Including totalCount
helps build accurate pagination controls such as “Total Pages” or “Results x‑y of z.”
Q5. Is Relay‑style pagination mandatory?
Not necessarily. It’s standardized and useful for Relay or Apollo clients, but simple cursor pagination works fine for most APIs.
Q6. How does Spring Data GraphQL simplify paginated sorting?
It maps GraphQL arguments to JPA queries automatically, leveraging Pageable
and Sort
. You get pagination, sorting, and counting with minimal boilerplate.
Q7. Can I handle compound sorting like createdAt DESC, name ASC
?
Yes. Your input type should allow multiple fields, and your resolver should pass them sequentially to your ORM’s sort logic.