Magento 2 GraphQL API: Complete Developer Guide (2026)
Magento 2’s GraphQL API is the backbone of modern headless storefronts, PWAs, and mobile apps. Unlike REST — which often requires multiple round-trips — GraphQL lets a client request exactly the fields it needs in a single call. This guide covers the built-in schema, authentication, cart and checkout mutations, custom resolver development, and the performance patterns that matter in production.
In short: GraphQL in Magento 2 exposes products, categories, cart, checkout, and customer data through a typed schema at
/graphql. Use queries for reads, mutations for writes, and extend the schema with custom resolvers when built-in fields are not enough.
Table of Contents
- Why GraphQL for Magento 2?
- Endpoint, Headers, and First Query
- Core Schema: Products, Categories, Search
- Cart and Checkout Mutations
- Customer Authentication
- Building a Custom Query Resolver
- Building a Custom Mutation
- Performance and Caching
- GraphQL vs REST: When to Use What
- Technical FAQ
Why GraphQL for Magento 2?
Adobe has invested heavily in GraphQL as the primary API for storefront-facing operations. REST remains available for Admin integrations and legacy systems, but new frontend work — Hyvä React Checkout, PWA Studio, custom Next.js storefronts — runs on GraphQL.
| Advantage | Detail |
|---|---|
| Single request | Fetch product name, price, image, and stock in one call |
| No over-fetching | Client selects only the fields it renders |
| Typed schema | Self-documenting via introspection |
| Cart state | cart_id token model fits stateless frontends |
| Version stability | Schema evolves with backward-compatible deprecations |
Endpoint:
POST https://your-store.com/graphql
Content-Type: application/json
Endpoint, Headers, and First Query
Basic product query
{
products(filter: { sku: { eq: "24-MB01" } }) {
items {
sku
name
price_range {
minimum_price {
regular_price {
value
currency
}
}
}
image {
url
label
}
}
}
}
Send it with curl:
curl -X POST https://your-store.com/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ products(filter: { sku: { eq: \"24-MB01\" } }) { items { sku name } } }"}'
Store header (multi-store setups)
When running multiple store views, pass the Store header:
-H "Store: default"
Without it, Magento may return data from the wrong store view — wrong prices, wrong language, wrong currency.
Core Schema: Products, Categories, Search
Category listing with filters
{
categoryList(filters: { url_key: { eq: "gear" } }) {
id
name
products(pageSize: 12, currentPage: 1) {
total_count
items {
sku
name
small_image { url }
price_range {
minimum_price {
final_price { value currency }
}
}
}
page_info {
current_page
total_pages
}
}
}
}
Full-text search
{
products(search: "backpack", pageSize: 10) {
total_count
items {
sku
name
url_key
}
}
}
Search relies on the catalogsearch_fulltext indexer and Elasticsearch/OpenSearch. If search returns empty results, reindex first:
bin/magento indexer:reindex catalogsearch_fulltext
Configurable product options
{
products(filter: { sku: { eq: "MH01" } }) {
items {
sku
name
... on ConfigurableProduct {
configurable_options {
attribute_code
label
values {
value_index
label
}
}
variants {
product {
sku
name
price_range {
minimum_price {
final_price { value }
}
}
}
}
}
}
}
}
Use inline fragments (... on ConfigurableProduct) to access type-specific fields — a core GraphQL pattern in Magento.
Cart and Checkout Mutations
Guest carts use a cart_id (masked quote ID). Customer carts use the authenticated customer token.
Create a guest cart
mutation {
createEmptyCart
}
Response:
{ "data": { "createEmptyCart": "abc123xyz" } }
Add a product to cart
mutation {
addProductsToCart(
cartId: "abc123xyz"
cartItems: [{ sku: "24-MB01", quantity: 1 }]
) {
cart {
items {
quantity
product {
name
sku
}
}
prices {
grand_total {
value
currency
}
}
}
}
}
Merge guest cart after login
mutation {
mergeCarts(
source_cart_id: "abc123xyz"
destination_cart_id: "customer-cart-id"
) {
items { quantity product { sku } }
}
}
Set shipping address and method
mutation {
setShippingAddressesOnCart(
input: {
cart_id: "abc123xyz"
shipping_addresses: [{
address: {
firstname: "John"
lastname: "Doe"
street: ["123 Main St"]
city: "Paris"
postcode: "75001"
country_code: FR
telephone: "0600000000"
}
}]
}
) {
cart {
shipping_addresses {
available_shipping_methods {
carrier_code
method_code
amount { value currency }
}
}
}
}
}
Headless checkout tip: Always request
available_shipping_methodsandavailable_payment_methodsafter setting the address — rates depend on the cart contents and destination.
Customer Authentication
Generate a customer token
mutation {
generateCustomerToken(
email: "[email protected]"
password: "Password123!"
) {
token
}
}
Use the token on subsequent requests:
-H "Authorization: Bearer <token>"
Tokens expire based on Admin configuration (Stores → Configuration → Services → OAuth → Customer Token Lifetime). Default is 1 hour — plan refresh logic in your frontend.
Introspection query (dev only)
Disable introspection in production for security. In development, explore the schema:
{
__schema {
types {
name
kind
}
}
}
Or use GraphQL Playground / Altair / Postman with introspection enabled.
Building a Custom Query Resolver
When the built-in schema is not enough, extend it with a custom module.
Directory structure
app/code/MagentoMastery/GraphQlDemo/
├── registration.php
├── etc/
│ ├── module.xml
│ └── schema.graphqls
└── Model/
└── Resolver/
└── HelloWorld.php
Query schema.graphqls
type Query {
helloWorld(name: String): String
@resolver(class: "MagentoMastery\\GraphQlDemo\\Model\\Resolver\\HelloWorld")
@doc(description: "Returns a greeting string")
}
Resolver class
<?php
declare(strict_types=1);
namespace MagentoMastery\GraphQlDemo\Model\Resolver;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
class HelloWorld implements ResolverInterface
{
public function resolve(
Field $field,
$context,
ResolveInfo $info,
?array $value = null,
?array $args = null
): string {
$name = $args['name'] ?? 'World';
return "Hello, {$name}!";
}
}
Test the custom query
{ helloWorld(name: "Magento") }
After deploying:
bin/magento setup:upgrade
bin/magento cache:flush
GraphQL schema changes require cache flush — Magento caches the compiled schema.
Building a Custom Mutation
Mutations follow the same pattern but implement ResolverInterface on a Mutation type field.
Mutation schema.graphqls
type Mutation {
subscribeNewsletter(email: String!): NewsletterOutput
@resolver(class: "MagentoMastery\\GraphQlDemo\\Model\\Resolver\\SubscribeNewsletter")
}
type NewsletterOutput {
success: Boolean!
message: String
}
Resolver with validation
<?php
declare(strict_types=1);
namespace MagentoMastery\GraphQlDemo\Model\Resolver;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
class SubscribeNewsletter implements ResolverInterface
{
public function resolve(
Field $field,
$context,
ResolveInfo $info,
?array $value = null,
?array $args = null
): array {
$email = $args['email'] ?? '';
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new GraphQlInputException(__('Invalid email address.'));
}
// Subscribe logic here...
return [
'success' => true,
'message' => 'Subscribed successfully.',
];
}
}
Throw GraphQlInputException for client errors (400-level) and GraphQlAuthorizationException for auth failures — Magento maps these to proper GraphQL error responses.
Performance and Caching
GraphQL can be slower than expected if clients request too many nested fields. Apply these rules in production:
1. Limit query depth and complexity
Use a reverse proxy or Magento’s built-in query complexity limits (Adobe Commerce) to block abusive queries.
2. Cache at the CDN for anonymous queries
Only cache queries that do not include customer tokens or cart IDs. Product listing queries are good CDN candidates when keyed by store + query hash.
3. Avoid N+1 in custom resolvers
When resolving lists, use batch resolvers or data loaders. A naive resolver that loads a product model per item will destroy performance on category pages.
4. Request only what you render
items { sku name description { html } }
items { sku name small_image { url } price_range { minimum_price { final_price { value } } } }
5. Keep indexers healthy
GraphQL reads from indexed flat tables and Elasticsearch. Stale indexers = stale API responses. See the indexers guide for maintenance.
GraphQL vs REST: When to Use What
| Use Case | Recommended API |
|---|---|
| Storefront / PWA / mobile app | GraphQL |
| Admin integrations (orders, catalog import) | REST (Async Bulk API) |
| Third-party ERP sync | REST |
| Real-time cart/checkout | GraphQL |
| Legacy integrations | REST |
Adobe’s direction is clear: GraphQL for customer-facing, REST for operational/back-office bulk operations.
Technical FAQ
Where is the GraphQL schema defined?
Core schema files live in each module’s etc/schema.graphqls. Magento merges them at runtime. Custom modules add their own etc/schema.graphqls.
Can I use GraphQL in Admin?
GraphQL is designed for storefront operations. Admin functionality uses REST or the Admin UI — do not expose Admin operations via custom GraphQL without strict ACL checks.
How do I debug GraphQL errors?
Enable developer mode and check var/log/exception.log. GraphQL returns errors in the errors array of the JSON response with message and category fields.
Does GraphQL replace Knockout.js checkout?
Not automatically. The Luma checkout is still Knockout-based. Headless checkout requires a frontend that consumes GraphQL mutations (React, Vue, Hyvä Checkout, etc.). See the checkout customization guide for working with Knockout.js or building a headless alternative.
Is GraphQL available in Magento Open Source?
Yes. GraphQL has been part of Magento Open Source since 2.3.x. Some advanced features (query complexity limits, staging previews) are Adobe Commerce only.
Conclusion
GraphQL is the standard API layer for modern Magento 2 storefronts. Master the built-in product and cart schema first, then extend with custom resolvers when business logic demands it. Keep queries lean, indexers current, and authentication tokens fresh — and your headless storefront will stay fast and reliable.
Building a headless Magento 2 storefront or need custom GraphQL endpoints? Contact me for architecture reviews, custom modules , and performance optimization.