Filtering and Sorting

Pagination usually becomes useful when combined with filtering and sorting.

A GraphQL API might define:

input ProductFilterInput {
  category: ProductCategory
  minPrice: Float
  maxPrice: Float
  search: String
}

enum ProductSortField {
  PRICE
  CREATED_AT
  NAME
}

enum SortDirection {
  ASC
  DESC
}

input ProductSortInput {
  field: ProductSortField!
  direction: SortDirection!
}

Then:

products(
  first: Int!
  after: String
  filter: ProductFilterInput
  sort: ProductSortInput
): ProductConnection!

The server should translate these inputs into controlled database operations rather than accepting arbitrary database syntax.

Sorting also needs deterministic tie-breaking.

For example, sorting only by createdAt can produce ambiguous ordering if multiple documents share the same timestamp.

A better database order might be:

createdAt DESC, _id DESC

The unique ID acts as a tie-breaker.

This matters especially for cursor pagination. If ordering is not deterministic, the same cursor can produce inconsistent page boundaries.

Typed Filtering and Sorting Input
input ProductFilterInput {
  category: ProductCategory
  minPrice: Float
  maxPrice: Float
  search: String
}

enum ProductSortField {
  PRICE
  CREATED_AT
  NAME
}

enum SortDirection {
  ASC
  DESC
}

input ProductSortInput {
  field: ProductSortField!
  direction: SortDirection!
}
💡 The API exposes a controlled set of filtering and sorting capabilities. Clients cannot directly provide arbitrary MongoDB or SQL expressions. The repository translates these typed values into database-specific queries.
Key Takeaways
  • Filters should be represented with explicit GraphQL input types.
  • Sort fields should come from a controlled enum where possible.
  • Deterministic ordering is important for reliable pagination.
  • Tie-breaker fields can make ordering stable.
Knowledge Check

Why is a unique tie-breaker useful when sorting records for cursor pagination?