skills/platform-magento/magento-graphql/SKILL.md
Query Magento's GraphQL API to build headless storefronts or PWA Studio frontends with products, cart, checkout, and customer operations
npx skillsauth add finsilabs/awesome-ecommerce-skills magento-graphqlInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Magento 2 (and Adobe Commerce) ships a comprehensive GraphQL API for storefront operations — product catalog, search, cart, checkout, customer authentication, and order history. The endpoint is /graphql and does not require an admin token for public catalog data. Cart and customer operations require a guest cart ID (UUID) or a Bearer token from customer login. PWA Studio (Venia) is built entirely on this API, making it the reference implementation for headless Magento.
Set up the GraphQL client
Magento GraphQL uses standard HTTP POST to /graphql. Use Apollo Client or a lightweight fetch wrapper:
// lib/magento.ts
const MAGENTO_URL = process.env.NEXT_PUBLIC_MAGENTO_URL; // e.g. https://magento.example.com
export async function magentoQuery<T>(
query: string,
variables: Record<string, unknown> = {},
token?: string
): Promise<T> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
Store: process.env.NEXT_PUBLIC_MAGENTO_STORE_CODE ?? "default",
};
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
const response = await fetch(`${MAGENTO_URL}/graphql`, {
method: "POST",
headers,
body: JSON.stringify({ query, variables }),
next: { revalidate: 300 }, // Next.js ISR caching
});
if (!response.ok) {
throw new Error(`Magento GraphQL HTTP error: ${response.status}`);
}
const { data, errors } = await response.json();
if (errors?.length) {
throw new Error(errors[0].message);
}
return data as T;
}
Query the product catalog
// Fetch products by category UID or search
export async function getProducts(params: {
categoryUid?: string;
search?: string;
pageSize?: number;
currentPage?: number;
}) {
return magentoQuery<{ products: MagentoProductList }>(`
query GetProducts(
$search: String
$filter: ProductAttributeFilterInput
$pageSize: Int
$currentPage: Int
) {
products(
search: $search
filter: $filter
pageSize: $pageSize
currentPage: $currentPage
sort: { position: ASC }
) {
total_count
page_info { current_page page_size total_pages }
aggregations {
attribute_code label count
options { label value count }
}
items {
uid sku name url_key
... on SimpleProduct {
price_range {
minimum_price {
regular_price { value currency }
final_price { value currency }
discount { percent_off amount_off }
}
}
}
... on ConfigurableProduct {
configurable_options {
attribute_code label
values { uid label swatch_data { value } }
}
variants {
attributes { uid label code value_index }
product { sku stock_status price_range { minimum_price { final_price { value } } } }
}
}
small_image { url label }
thumbnail { url label }
rating_summary review_count
stock_status
}
}
}
`, {
search: params.search,
filter: params.categoryUid ? { category_uid: { eq: params.categoryUid } } : undefined,
pageSize: params.pageSize ?? 20,
currentPage: params.currentPage ?? 1,
});
}
Create guest cart and add items
// Create a guest cart and get the cart ID
export async function createGuestCart(): Promise<string> {
const data = await magentoQuery<{ createEmptyCart: string }>(`
mutation { createEmptyCart }
`);
return data.createEmptyCart; // Returns a UUID cart ID
}
// Add a simple product to cart
export async function addSimpleProductToCart(cartId: string, sku: string, qty: number) {
return magentoQuery(`
mutation AddSimpleProduct($cartId: String!, $sku: String!, $qty: Float!) {
addSimpleProductsToCart(
input: {
cart_id: $cartId
cart_items: [{ data: { sku: $sku, quantity: $qty } }]
}
) {
cart {
items {
uid quantity
product { name sku }
prices { price { value currency } }
}
prices {
subtotal_excluding_tax { value currency }
grand_total { value currency }
}
}
}
}
`, { cartId, sku, qty });
}
// Add a configurable product with selected variant
export async function addConfigurableProductToCart(
cartId: string,
parentSku: string,
variantSku: string,
qty: number
) {
return magentoQuery(`
mutation AddConfigurable($cartId: String!, $parentSku: String!, $variantSku: String!, $qty: Float!) {
addConfigurableProductsToCart(
input: {
cart_id: $cartId
cart_items: [{
parent_sku: $parentSku
data: { sku: $variantSku, quantity: $qty }
}]
}
) {
cart {
items { uid quantity product { name } }
}
}
}
`, { cartId, parentSku, variantSku, qty });
}
Authenticate customers and manage accounts
// Customer login — returns a bearer token
export async function loginCustomer(email: string, password: string): Promise<string> {
const data = await magentoQuery<{ generateCustomerToken: { token: string } }>(`
mutation Login($email: String!, $password: String!) {
generateCustomerToken(email: $email, password: $password) {
token
}
}
`, { email, password });
return data.generateCustomerToken.token;
}
// Get authenticated customer's cart
export async function getCustomerCart(token: string) {
return magentoQuery(`
query {
customerCart {
id
items {
uid quantity
product { name sku small_image { url } }
prices { price { value currency } }
}
prices { grand_total { value currency } }
}
}
`, {}, token);
}
// Get order history
export async function getCustomerOrders(token: string, pageSize = 10) {
return magentoQuery(`
query GetOrders($pageSize: Int) {
customer {
orders(pageSize: $pageSize, sort: { sort_field: CREATED_AT, sort_direction: DESC }) {
total_count
items {
id number status order_date
total { grand_total { value currency } }
items { product_name product_sku quantity_ordered }
shipping_address { firstname lastname city region { code } country_code }
}
}
}
}
`, { pageSize }, token);
}
Create a custom GraphQL resolver in a Magento 2 module
<?php
// app/code/MyVendor/CustomGraphQL/etc/schema.graphqls
type Query {
myCustomProducts(
brand: String @doc(description: "Filter by brand")
): MyCustomProductOutput @resolver(class: "MyVendor\\CustomGraphQL\\Model\\Resolver\\CustomProducts") @doc(description: "Get custom product list")
}
type MyCustomProductOutput {
items: [CustomProductItem]
total_count: Int
}
type CustomProductItem {
sku: String
name: String
brand: String
custom_attribute: String
}
<?php
// app/code/MyVendor/CustomGraphQL/Model/Resolver/CustomProducts.php
namespace MyVendor\CustomGraphQL\Model\Resolver;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
class CustomProducts implements ResolverInterface
{
public function __construct(
private readonly CollectionFactory $collectionFactory
) {}
public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null)
{
$collection = $this->collectionFactory->create();
$collection->addAttributeToSelect(['sku', 'name', 'brand', 'custom_attribute']);
$collection->addAttributeToFilter('status', 1);
$collection->addAttributeToFilter('visibility', ['neq' => 1]);
if (!empty($args['brand'])) {
$collection->addAttributeToFilter('brand', ['like' => '%' . $args['brand'] . '%']);
}
$items = [];
foreach ($collection as $product) {
$items[] = [
'sku' => $product->getSku(),
'name' => $product->getName(),
'brand' => $product->getData('brand'),
'custom_attribute' => $product->getData('custom_attribute'),
];
}
return ['items' => $items, 'total_count' => $collection->getSize()];
}
}
export async function getProductByUrlKey(urlKey: string) {
return magentoQuery<{ products: { items: MagentoProduct[] } }>(`
query GetProduct($urlKey: String!) {
products(filter: { url_key: { eq: $urlKey } }) {
items {
uid sku name url_key
meta_title meta_description
description { html }
short_description { html }
... on ConfigurableProduct {
configurable_options {
id uid label attribute_code position
values {
uid label
swatch_data {
... on ColorSwatchData { value }
... on ImageSwatchData { thumbnail value }
... on TextSwatchData { value }
}
}
}
variants {
attributes { uid label code value_index }
product {
uid sku stock_status
media_gallery { url label disabled }
price_range {
minimum_price {
regular_price { value currency }
final_price { value currency }
}
}
}
}
}
media_gallery { url label disabled position }
reviews(pageSize: 5) {
items {
summary text created_at
ratings_breakdown { name value }
nickname
}
}
}
}
}
`, { urlKey });
}
export async function setShippingAddress(cartId: string, address: AddressInput) {
return magentoQuery(`
mutation SetShipping($cartId: String!, $address: CartAddressInput!) {
setShippingAddressesOnCart(input: {
cart_id: $cartId
shipping_addresses: [{ address: $address }]
}) {
cart {
shipping_addresses {
available_shipping_methods {
carrier_code method_code carrier_title method_title
amount { value currency }
}
}
}
}
}
`, { cartId, address });
}
export async function placeOrder(cartId: string) {
return magentoQuery<{ placeOrder: { order: { order_number: string } } }>(`
mutation PlaceOrder($cartId: String!) {
placeOrder(input: { cart_id: $cartId }) {
order { order_number }
errors { message code }
}
}
`, { cartId });
}
Store HTTP header to target specific store views — without it, Magento defaults to the default store and returns incorrect pricing/currency for multi-store setups... on SimpleProduct, ... on ConfigurableProduct, ... on BundleProduct where applicablemergeCarts mutationerrors array — GraphQL responses return 200 even for errors; always check errors property in the response bodyhttpOnly cookies, not localStorage, to prevent XSS token theft| Problem | Solution |
|---------|----------|
| Products missing from GraphQL but visible in Admin | Check the product's visibility attribute — products set to "Not Visible Individually" won't appear in catalog queries |
| Configurable product variants return empty stock_status | Query the product field inside variants.product — stock is tracked at the simple product (variant) level, not the parent |
| Custom resolver not recognized | Run bin/magento setup:upgrade after adding new GraphQL schema files; also check schema.graphqls syntax carefully |
| Cart merge fails after customer login | Call mergeCarts(source_cart_id: $guestCartId, destination_cart_id: $customerCartId) — then discard the guest cart ID cookie |
| Slow GraphQL responses | Enable Magento's built-in GraphQL caching: bin/magento config:set system/full_page_cache/caching_application 1; use Varnish for public queries |
| Bearer token expired mid-session | Implement token refresh: catch The current customer isn't authorized error and redirect to login or use refresh token if available |
tools
Let shoppers save products to a wishlist, share it with friends, and get notified when saved items come back in stock or drop in price
development
Build a themeable storefront with design tokens and CSS custom properties that supports white-labeling, multi-brand variants, and dark mode
development
Speed up product discovery with instant search suggestions, fuzzy typo matching, and category-aware results powered by Algolia or Elasticsearch
development
Build a mobile-first storefront with thumb-friendly navigation, sticky add-to-cart buttons, and touch-optimized components for high mobile conversion