GraphQL query builder

Headless Adobe Commerce storefronts query the catalog over GraphQL. Build a query visually (or write it by hand): the server processes it with a custom parser written in PHP, validates it against a fixed schema, enforces depth and complexity limits, then executes it with batched resolvers. Next to the response you see the execution plan and the timings.

Query

Basic list
N+1 and batching
Filters + aggregations
Too deep
Too complex
Syntax error
Category (category_uid)

Color

red
blue
black
white
green

Size

S
M
L
XL

Brand

Aurora
Borealis
Corona
Sort
pageSize

Selected fields

The generated GraphQL query
{
  products(pageSize: 5) {
    total_count
    page_info { current_page page_size total_pages }
    items {
      sku
      name
      price_range {
        minimum_price {
          regular_price { value currency }
          final_price { value currency }
        }
      }
      categories { name url_key }
    }
  }
}

The fixed schema (excerpt)

type Query {
  products(search: String, filter: ProductAttributeFilterInput,
           pageSize: Int = 20, currentPage: Int = 1,
           sort: ProductAttributeSortInput): Products
}
type Products {
  items: [ProductInterface]  total_count: Int
  page_info: SearchResultPageInfo  aggregations: [Aggregation]
}
type ProductInterface {
  uid: ID  sku: String  name: String  stock_status: ProductStockStatus
  color: String  size: String  brand: String  price_range: PriceRange
  categories: [CategoryTree]  related_products: [ProductInterface]
}
type CategoryTree { uid: ID  name: String  url_key: String  product_count: Int }
input ProductAttributeFilterInput {
  category_uid: FilterEqualTypeInput  color: FilterEqualTypeInput
  size: FilterEqualTypeInput  brand: FilterEqualTypeInput
  price: FilterRangeTypeInput
}
input ProductAttributeSortInput { relevance: SortEnum  name: SortEnum  price: SortEnum }

What does the server do with a GraphQL request?

The request is text that the server does not run but parses: the lexer splits it into tokens, the parser builds a syntax tree, and the validator checks it against the fixed schema (existing fields, correct argument types, the rules for leaf and composite fields). There is no eval and no dynamic class or method name: only fields fixed in the schema are resolved, by fixed resolvers.

The biggest risk of a public GraphQL endpoint is the expensive query: a few lines of text that, through nested lists, would fetch thousands of objects. So two limits run BEFORE execution: depth (at most 6 levels here) and complexity, where every field is multiplied by the expected size of the lists above it. Adobe Commerce also enforces depth and complexity limits on GraphQL requests.

The N+1 problem: if categories or related products were queried separately per product, a page of 20 would run 20 extra queries per field. The executor therefore walks level by level and calls a field resolver once for all parents on that level (the DataLoader pattern); Magento solves the same with batch resolvers.

The demo implements a well-defined subset: a single query operation, fields, aliases and literal arguments. Variables, fragments, directives, mutations and introspection are left out. They are needed for a complete language, but would grow the parser and the attack surface without enriching the point (validation, limits, batched execution).