GraphQL in Magento
Magento serves one GraphQL endpoint at /graphql, with a typed schema stitched together from every enabled module.
POST /graphql with {"query": "..."} | GET /graphql?query={...}GraphQL is a query language for an API. The client sends one document listing the fields it wants, and the server returns those fields and nothing else. Magento 2.4.9 exposes it at a single URL, /graphql.
That URL is an area: one of Magento's request contexts, each with its own front name and configuration. Magento_GraphQl registers an area whose front name is graphql, so every request lands on the same path. Inside the document a query reads data, a mutation writes it, and a resolver is the PHP class that produces the value of one field.
Why a decoupled storefront prefers it
A product page needs the product, its price, its media and its URL. Over REST that is several requests whose responses you cannot trim. One GraphQL document asks for all of it and returns only the named fields, which matters on a phone. PWA Studio uses GraphQL exclusively; Hyvä uses it for cart and checkout.
What it does not cover
Magento's GraphQL is a storefront API. There is no admin schema, so order management and catalog editing still go through the REST API. Two limits are set in Magento_GraphQl's etc/di.xml: a query may be 20 levels deep, and its field count may not exceed 1000.
Making a schema change visible
The stitched schema is stored in the configuration cache. After editing any schema.graphqls, run bin/magento cache:clean config. A brand new module needs bin/magento setup:upgrade first.
Example
# A GraphQL request document, sent to /graphql. It is not a file in your module.
# Shorthand: an anonymous operation. The leading "query" keyword is implied.
{
products(filter: { sku: { eq: "24-MB01" } }, pageSize: 1) {
total_count
items {
sku
name
price_range {
minimum_price {
regular_price { value currency }
}
}
}
}
}
# Longhand: the same request written as a named query operation.
# These two are the same thing; the name only shows up in logs and errors.
query ProductBySku {
products(filter: { sku: { eq: "24-MB01" } }, pageSize: 1) {
total_count
items {
sku
name
price_range {
minimum_price {
regular_price { value currency }
}
}
}
}
}
# Send one or the other. GraphQL forbids mixing an anonymous operation
# with named ones in a single document.When to use it
- Fetch a product's name, price and image in one request for a React or Vue storefront.
- Decide whether a new feature belongs in the GraphQL storefront schema or the REST admin API.
- Diagnose a query rejected for exceeding the 20-level depth limit after nesting a category tree.
- Confirm the endpoint and store headers work before debugging the client application.
More examples
The response mirrors the request
The shape of the response is the shape of the query. There is no envelope and no field you did not ask for. When something goes wrong, a sibling "errors" array appears alongside "data", and "data" can still hold the fields that did resolve, so a client has to inspect both.
{
"data": {
"products": {
"total_count": 1,
"items": [
{
"sku": "24-MB01",
"name": "Joust Duffle Bag",
"price_range": {
"minimum_price": {
"regular_price": { "value": 34, "currency": "USD" }
}
}
}
]
}
}
}A mutation, in two steps
Writes are mutations and must be sent as POST. Note user_errors: Magento returns expected problems, such as a product being out of stock, as data on the output type rather than as GraphQL errors. A client that only checks the errors array will silently miss them.
# createEmptyCart is deprecated in 2.4.x; createGuestCart replaces it.
mutation StartCart {
createGuestCart {
cart { id }
}
}
mutation AddItem {
addProductsToCart(
cartId: "the id returned above"
cartItems: [{ sku: "24-MB01", quantity: 1 }]
) {
cart {
itemsV2 {
total_count
items { uid quantity }
}
}
user_errors { code message }
}
}The headers that change the answer
The same query returns different data depending on these headers. Store selects the store view, Content-Currency the currency, and Authorization identifies the customer. Content-Type must be application/json: Magento_GraphQl registers a ContentTypeValidator that rejects a POST without it. In 2.4.9 it throws UnsupportedMediaTypeException, so the client sees HTTP 415 with the message "Request content type must be application/json" - the usual cause of a confusing failure from a hand-rolled client. Older 2.4 releases threw GraphQlInputException for the same mistake and returned 400.
curl -s -X POST https://example.test/graphql \
-H 'Content-Type: application/json' \
-H 'Store: default' \
-H 'Content-Currency: USD' \
-H 'Authorization: Bearer <customer token>' \
--data '{"query":"{ customer { firstname } }"}'
Discussion