Modeling Filtering and Sorting in GraphQL APIs
AI generated
{ }
type
GraphQL · Filtering · Sorting · Pagination · Input Types
Modeling Filtering and Sorting
in GraphQL APIs

How to model filter and sort arguments as clean input types, implement operator patterns for different data types, use Magento's filter conventions, and choose the right pagination strategy for each use case.

14 min read Input Types · Operators · Cursor Pagination · Magento Filters · Sorting GraphQL · Magento 2.4 · API Design

1. Why filter design is underestimated

Filter and sort logic in GraphQL APIs looks trivial at first glance: a few arguments, a bit of SQL in the resolver, done. In practice, however, poor filter design is one of the most common reasons APIs become hard to extend, create performance problems, and frustrate frontend teams. A query that passes filter conditions as flat strings is hard to validate, hard to document, and impossible to process programmatically.

The counterpart to this is typed filter design with structured input types and operator patterns. This approach is explicit, machine readable, validatable, and extensible. Magento leads the way here: FilterEqualTypeInput, FilterRangeTypeInput, and FilterMatchTypeInput are typed filter input types used consistently across all product list queries. Anyone building their own APIs benefits from adopting this pattern, adapted to their own data context.

2. Input types as the foundation for filter logic

The first step toward clean filter design is choosing input types over scalar arguments. Instead of query products(priceMin: Float, priceMax: Float), you model query products(filter: ProductFilterInput), where ProductFilterInput is an input type with fields for various filter criteria. This approach is more flexible (new filter fields can be added without changing the query signature) and clearer to document, because every filter field has an explicit name and type.

Input types in GraphQL follow the same type rules as output types, but they are only allowed as input. They can contain other input types as fields, but no output types. This allows filter composition: a ProductFilterInput can contain a field price: PriceRangeFilterInput, which in turn defines from: Float and to: Float. This nesting is clear, type safe, and semantically unambiguous.


# Typed filter input types, composable and self-documenting
input PriceRangeFilterInput {
    from: Float
    to: Float
}

input StringFilterInput {
    eq: String         # exact match
    in: [String!]      # match any in list
    like: String       # pattern match (% wildcard)
    neq: String        # not equal
}

input IntFilterInput {
    eq: Int
    gt: Int            # greater than
    lt: Int            # less than
    gte: Int           # greater than or equal
    lte: Int           # less than or equal
    in: [Int!]
}

input ProductFilterInput {
    sku: StringFilterInput
    name: StringFilterInput
    price: PriceRangeFilterInput
    category_id: IntFilterInput
    status: IntFilterInput
}

extend type Query {
    filteredProducts(
        filter: ProductFilterInput
        sort: ProductSortInput
        pageSize: Int = 20
        currentPage: Int = 1
    ): ProductSearchResult
        @resolver(class: "Vendor\\Catalog\\Model\\Resolver\\FilteredProducts")
}

3. Operator patterns: eq, in, range, like

Operator patterns structure filter conditions according to their semantics. The simplest operator is eq (exact match): a field must match a value exactly. For list-based filtering, in is the right operator: the field must match one of the values in the list. For numeric ranges such as prices or dates, from/to or gte/lte pairs are suitable. For text search, like with wildcard support is the standard.

An important aspect of operator patterns is how null values are handled. In Magento, filter fields that are not supplied are simply ignored: no filter means no restriction. That makes sense for optional filter fields, but it is a security gap for mandatory filters, such as a tenant filter for multi-store setups. Mandatory filters should be declared as non-nullable arguments rather than as optional fields inside an input type, or the resolver must explicitly check that the mandatory filter is set.

4. Magento's FilterEqualTypeInput: using the conventions

Magento defines several reusable filter input types in Magento_GraphQl/etc/schema.graphqls: FilterEqualTypeInput (with eq and in), FilterRangeTypeInput (with from and to), and FilterMatchTypeInput (with match for full-text search). These types are used in Magento's product filters and are the standard for custom filters in Magento modules that should stay consistent with the core schema.


# Using Magento's built-in filter types for consistency
# These types are defined in Magento_GraphQl/etc/schema.graphqls

input CustomProductFilterInput {
    # Exact match or list: use Magento's FilterEqualTypeInput
    category_uid: FilterEqualTypeInput
    color: FilterEqualTypeInput
    size: FilterEqualTypeInput

    # Numeric range: use Magento's FilterRangeTypeInput
    price: FilterRangeTypeInput
    stock_quantity: FilterRangeTypeInput

    # Full-text match: use Magento's FilterMatchTypeInput
    name: FilterMatchTypeInput
    description: FilterMatchTypeInput
}

# Example query using the filter
query FilteredProductList {
  filteredProducts(
    filter: {
      category_uid: { eq: "MTI=" }
      price: { from: "10.00", to: "100.00" }
      color: { in: ["red", "blue"] }
      name: { match: "running" }
    }
    sort: { name: ASC }
    pageSize: 20
    currentPage: 1
  ) {
    total_count
    items {
      sku
      name
      price_range { minimum_price { final_price { value currency } } }
    }
  }
}

5. Modeling sorting: enum fields and direction

Sort logic in GraphQL is modeled most cleanly with a dedicated sort input type. The sort fields are enums, which prevents invalid field names and makes the allowed sort options visible to the client. Sort direction is expressed through a separate enum type, SortEnum, with the values ASC and DESC. Magento uses this pattern in its product API, and it transfers well to other resource types.

A common design question is whether multi-column sorting (sorting by several fields at once) should be supported. Magento supports it: the sort input type can have multiple fields set, and the resolver interprets them as a prioritized sort order. If you do not need multi-column sorting, you should still design the type so it can be extended later, meaning you use an input type rather than a scalar argument for sorting.

6. Pagination: offset vs. cursor based

Pagination is its own design topic in GraphQL. The simplest form is offset pagination with pageSize and currentPage: understandable, widely used in Magento, but with a well-known problem for live data. If a record is added or removed between two page requests, the offset shifts and the client ends up seeing either duplicate or skipped entries. For product lists that change rarely, that is acceptable; for activity feeds or real-time lists, it is problematic.

Cursor-based pagination is the more robust pattern, defined in the Relay specification: the server attaches an opaque cursor to each record, and the client can pass this cursor as the starting point for the next page. This is stable against insertions and deletions during pagination. The implementation is more involved (the cursor has to encode the sort position and be decoded on the next request), but the result is consistent pagination regardless of data changes.


# Offset-based pagination (simple, Magento-native)
type ProductSearchResult {
    items: [Product!]!
    total_count: Int!
    page_info: SearchResultPageInfo!
}

type SearchResultPageInfo {
    page_size: Int!
    current_page: Int!
    total_pages: Int!
}

# Cursor-based pagination (Relay spec, more robust for live data)
type ProductConnection {
    edges: [ProductEdge!]!
    pageInfo: PageInfo!
    totalCount: Int!
}

type ProductEdge {
    node: Product!
    cursor: String!   # opaque cursor, do not parse client-side
}

type PageInfo {
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
    startCursor: String
    endCursor: String
}

# Query using cursor pagination
extend type Query {
    productsFeed(
        first: Int = 20
        after: String   # cursor from previous page's endCursor
        filter: ProductFilterInput
    ): ProductConnection
}

7. Wiring filters to the resolver and repository

The resolver takes the structured filter input from $args['filter'] and has to translate it into database queries. In Magento, that is the SearchCriteriaBuilder: each filter field is translated into a FilterGroup entry. Mapping GraphQL filter fields to Magento attribute codes is its own concern; it belongs in a dedicated mapper class, not directly in the resolver. This mapper is also the right place to explicitly exclude filter fields that are not allowed.

Performance is critical for filtered lists: a filter on a non-indexed attribute leads to a full table scan. The resolver cannot prevent that, but it can refuse it by rejecting non-indexed filter fields with a GraphQlInputException. A whitelist of allowed filter fields, analogous to a whitelist of allowed sort fields, is the right pattern here. Anything not explicitly allowed gets rejected.

8. Security: what must not be filterable

Filters are an attack surface for data exposure: a client could try to filter by internal fields that exist in the database schema but are not exposed in the GraphQL schema. This is especially relevant in SQL-based backends where the resolver translates filters directly into SQL conditions. A whitelist of allowed filter fields prevents a client from filtering by fields such as password_hash, fraud_score, or other internal attributes.

A second security aspect is the complexity of filter expressions. A client could pass a filter with hundreds of in values, generating an expensive SQL IN clause. A maximum number of values in the in operator, for example 100, prevents intentional or unintentional query overload. This limit should be checked explicitly in the resolver or mapper and rejected with a GraphQlInputException when exceeded.

9. Summary

Modeling filtering and sorting cleanly in GraphQL APIs means: typed input types instead of flat scalar arguments, operator patterns instead of implicit filter logic, whitelisting instead of free field selection, and the right pagination strategy for the use case. Magento's filter input types are a good reference model for custom filters in Magento modules and for external APIs that need similar filtering concepts.

The effort spent on clean filter design pays off twice over: the frontend team gets a clear, documented API that does not need to learn the backend's implementation details. The backend team can perform performance optimizations at the repository layer without changing the API surface. And security properties, such as whitelisting and complexity limits, are explicit and testable instead of implicit and accidental.

Filtering and Sorting in GraphQL APIs: The Essentials at a Glance

Input Types

Typed filter input types instead of scalar arguments. Composition through nested input types. Use Magento's FilterEqualTypeInput as a template.

Operators

eq, in, range, like as dedicated fields in the filter input type. Fields not supplied are ignored. Mandatory filters as non-nullable arguments.

Pagination

Offset (pageSize/currentPage) for simple lists. Cursor-based (Relay spec) for live data and consistent navigation without duplicates.

Security

Whitelist of allowed filter fields in the mapper. Cap the maximum in-operator size. Explicitly reject non-indexed fields.

10. Comparison: filter modeling approaches

Approach Example Advantage Disadvantage
Scalar arguments products(sku: "ABC") Simple Not extensible, no operators
Input type with operator filter: { sku: { eq: "ABC" } } Type safe, extensible More schema code
Magento FilterEqualTypeInput { eq: "x", in: ["x","y"] } Core compatible, reusable Magento specific
Offset pagination pageSize: 20, currentPage: 2 Simple, understandable Inconsistent with live data
Cursor pagination first: 20, after: "cursor" Stable, Relay compatible More complex implementation

11. FAQ: Modeling Filtering and Sorting in GraphQL APIs

1Why input types instead of scalar arguments?
Input types are type safe, self-documenting, and extensible. New filter options can be added without changing the query signature. Scalar arguments do not scale.
2What is Magento's FilterEqualTypeInput?
A built-in Magento input type with eq and in. The standard for custom filters in Magento modules. Consistent with the core product API.
3Cursor instead of offset pagination?
For live data that can change during pagination. For static product lists, offset is sufficient and simpler to implement.
4Preventing filters on internal fields?
Implement a whitelist of allowed filter fields in the mapper. Not on the list: GraphQlInputException before the database call.
5Translating GraphQL filters into Magento SearchCriteria?
In a dedicated mapper class, not in the resolver. The mapper builds FilterGroup objects for the SearchCriteriaBuilder and is testable without GraphQL context.
6Filter on a non-indexed attribute?
Leads to a full table scan and poor performance. Explicitly reject non-indexed fields or ensure a database index exists for every allowed filter field.
7Multiple sort fields at once?
Yes. Sort input type with several optional fields. The resolver interprets all set fields as a prioritized order. Magento supports this natively.
8Modeling a mandatory filter?
As a non-nullable query argument, not as an optional field in the filter input type. Example: products(storeId: ID!, filter: ...), storeId cannot be omitted.
9Maximum values in an in-operator?
Rule of thumb: 100 to 200. Beyond that, large SQL IN clauses cause performance problems. Make the limit configurable, not hardcoded.
10Documenting filter input types for frontend teams?
@doc directives on all input fields. GraphiQL and Apollo Studio show this documentation directly from introspection. Store example queries in a schema registry.