Skip to content
advanced Phase 119 · Senior Projects

Project - Headless Magento Storefront

Build a headless Magento storefront with React, GraphQL, PWA, and custom theme architecture

3h
0 problems
Topic Progress 0%

Headless Architecture Setup

Architecture Overview

┌─────────────────────────────────────────────────────┐
│                   React Storefront                    │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐          │
│  │ Product  │  │   Cart   │  │ Checkout │          │
│  │ Listing  │  │  Module  │  │  Module  │          │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘          │
│       │              │              │                 │
│       └──────────────┼──────────────┘                 │
│                      │                               │
│              ┌───────┴───────┐                       │
│              │  Apollo Client │                       │
│              │  (GraphQL)    │                       │
│              └───────┬───────┘                       │
└──────────────────────┼───────────────────────────────┘
                       │
                ┌──────┴──────┐
                │   Magento   │
                │  GraphQL    │
                │  Endpoint   │
                └──────┬──────┘
                       │
                ┌──────┴──────┐
                │  Magento 2  │
                │  Backend    │
                └─────────────┘

Project Structure

magento-headless/
├── src/
│   ├── components/
│   │   ├── Product/
│   │   │   ├── ProductCard.tsx
│   │   │   ├── ProductList.tsx
│   │   │   └── ProductDetail.tsx
│   │   ├── Cart/
│   │   │   ├── CartDrawer.tsx
│   │   │   ├── CartItem.tsx
│   │   │   └── CartSummary.tsx
│   │   ├── Checkout/
│   │   │   ├── CheckoutFlow.tsx
│   │   │   ├── AddressForm.tsx
│   │   │   └── PaymentMethod.tsx
│   │   ├── Header/
│   │   │   ├── Navigation.tsx
│   │   │   └── MiniCart.tsx
│   │   └── common/
│   │       ├── Loading.tsx
│   │       └── ErrorBoundary.tsx
│   ├── graphql/
│   │   ├── queries/
│   │   │   ├── GET_PRODUCTS.ts
│   │   │   ├── GET_PRODUCT.ts
│   │   │   └── GET_CART.ts
│   │   ├── mutations/
│   │   │   ├── ADD_TO_CART.ts
│   │   │   ├── CREATE_CART.ts
│   │   │   └── PLACE_ORDER.ts
│   │   └── client.ts
│   ├── hooks/
│   │   ├── useCart.ts
│   │   ├── useAuth.ts
│   │   └── useProducts.ts
│   ├── pages/
│   │   ├── Home.tsx
│   │   ├── ProductList.tsx
│   │   ├── ProductDetail.tsx
│   │   ├── Cart.tsx
│   │   └── Checkout.tsx
│   ├── utils/
│   │   ├── auth.ts
│   │   ├── cart.ts
│   │   └── pushNotifications.ts
│   └── App.tsx
├── public/
│   ├── manifest.json
│   └── offline.html
├── package.json
└── tsconfig.json

The React frontend communicates exclusively via GraphQL to the Magento backend, enabling independent deployment and scaling.

GraphQL Queries and Mutations

Product Queries

// src/graphql/queries/GET_PRODUCTS.ts
import { gql } from '@apollo/client';

export const GET_PRODUCTS = gql`
    query GetProducts(
        $search: String
        $filter: ProductAttributeFilterInput
        $sort: ProductAttributeSortInput
        $pageSize: Int
        $currentPage: Int
    ) {
        products(
            search: $search
            filter: $filter
            sort: $sort
            pageSize: $pageSize
            currentPage: $currentPage
        ) {
            total_count
            items {
                id
                name
                sku
                url_key
                price_range {
                    minimum_price {
                        regular_price { value currency }
                        final_price { value currency }
                        discount { amount_off percent_off }
                    }
                }
                image {
                    url
                    label
                }
                stock_status
                ... on ConfigurableProduct {
                    variants {
                        product {
                            sku
                            name
                            stock_status
                            price_range {
                                minimum_price {
                                    final_price { value currency }
                                }
                            }
                        }
                        attributes {
                            label
                            code
                        }
                    }
                }
            }
            page_info {
                current_page
                page_size
                total_pages
            }
        }
    }
`;

export const GET_PRODUCT = gql`
    query GetProduct($urlKey: String!) {
        products(filter: { url_key: { eq: $urlKey } }) {
            items {
                id
                name
                sku
                url_key
                description { html }
                short_description { html }
                price_range {
                    minimum_price {
                        regular_price { value currency }
                        final_price { value currency }
                    }
                }
                media_gallery {
                    url
                    label
                    position
                }
                stock_status
                ... on ConfigurableProduct {
                    configurable_options {
                        attribute_code
                        label
                        values { label value_index }
                    }
                }
            }
        }
    }
`;

Cart Mutations

// src/graphql/mutations/ADD_TO_CART.ts
import { gql } from '@apollo/client';

export const CREATE_CART = gql`
    mutation CreateCart {
        createEmptyCart
    }
`;

export const ADD_TO_CART = gql`
    mutation AddToCart($cartId: String!, $cartItems: [CartInput!]!) {
        addProductsToCart(cartId: $cartId, cartItems: $cartItems) {
            cart {
                items {
                    uid
                    product { name sku }
                    quantity
                    prices { row_total { value currency } }
                }
                prices {
                    grand_total { value currency }
                    subtotal_excluding_tax { value currency }
                }
            }
        }
    }
`;

export const PLACE_ORDER = gql`
    mutation PlaceOrder($cartId: String!) {
        placeOrder(input: { cart_id: $cartId }) {
            order {
                order_number
                order_id
            }
        }
    }
`;

GraphQL provides exactly the data needed per request, reducing over-fetching compared to REST endpoints.

React Components and Hooks

Product Card Component

// src/components/Product/ProductCard.tsx
import React from 'react';
import { Link } from 'react-router-dom';
import { useCart } from '../../hooks/useCart';

interface Product {
    id: string;
    name: string;
    sku: string;
    url_key: string;
    price_range: {
        minimum_price: {
            final_price: { value: number; currency: string };
            discount?: { amount_off: number; percent_off: number };
        };
    };
    image: { url: string; label: string };
    stock_status: string;
}

interface ProductCardProps {
    product: Product;
}

export const ProductCard: React.FC<ProductCardProps> = ({ product }) => {
    const { addToCart, loading } = useCart();
    const price = product.price_range.minimum_price;

    const handleAddToCart = async () => {
        await addToCart(product.sku, 1);
    };

    return (
        <div className="product-card">
            <Link to={`/product/${product.url_key}`}>
                <div className="product-image">
                    <img src={product.image.url} alt={product.image.label} />
                    {price.discount && (
                        <span className="discount-badge">
                            -{price.discount.percent_off}%
                        </span>
                    )}
                </div>
                <div className="product-info">
                    <h3 className="product-name">{product.name}</h3>
                    <div className="product-price">
                        <span className="final-price">
                            {price.final_price.currency} {price.final_price.value.toFixed(2)}
                        </span>
                        {price.discount && (
                            <span className="regular-price">
                                {price.regular_price?.value.toFixed(2)}
                            </span>
                        )}
                    </div>
                </div>
            </Link>
            <button
                onClick={handleAddToCart}
                disabled={loading || product.stock_status !== 'IN_STOCK'}
                className="add-to-cart-btn"
            >
                {product.stock_status === 'IN_STOCK' ? 'Add to Cart' : 'Out of Stock'}
            </button>
        </div>
    );
};

useCart Hook

// src/hooks/useCart.ts
import { useState, useEffect } from 'react';
import { useMutation, useQuery } from '@apollo/client';
import { CREATE_CART, ADD_TO_CART } from '../graphql/mutations/ADD_TO_CART';
import { GET_CART } from '../graphql/queries/GET_CART';

export const useCart = () => {
    const [cartId, setCartId] = useState<string | null>(
        () => localStorage.getItem('cart_id')
    );

    const [createCart] = useMutation(CREATE_CART);
    const [addProductsMutation, { loading }] = useMutation(ADD_TO_CART);

    const { data: cartData, refetch } = useQuery(GET_CART, {
        variables: { cartId },
        skip: !cartId,
    });

    useEffect(() => {
        const initCart = async () => {
            if (!cartId) {
                const { data } = await createCart();
                const newCartId = data.createEmptyCart;
                localStorage.setItem('cart_id', newCartId);
                setCartId(newCartId);
            }
        };
        initCart();
    }, [cartId, createCart]);

    const addToCart = async (sku: string, quantity: number) => {
        if (!cartId) return;
        await addProductsMutation({
            variables: { cartId, cartItems: [{ sku, quantity }] },
        });
        refetch();
    };

    return {
        cart: cartData?.cart,
        addToCart,
        loading,
        itemCount: cartData?.cart?.items?.length || 0,
    };
};

Custom hooks encapsulate GraphQL interactions. Cart ID persists in localStorage for cross-session continuity.

PWA Features and Performance

Service Worker Configuration

// src/service-worker.ts
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate, CacheFirst } from 'workbox-strategies';

// Precache build files
precacheAndRoute(self.__WB_MANIFEST);

// Cache product images
registerRoute(
    ({ url }) => url.pathname.startsWith('/media/catalog/product/'),
    new CacheFirst({
        cacheName: 'product-images',
        plugins: [
            new ExpirationPlugin({
                maxEntries: 1000,
                maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
            }),
        ],
    })
);

// Cache GraphQL responses
registerRoute(
    ({ url }) => url.pathname === '/graphql',
    new StaleWhileRevalidate({
        cacheName: 'graphql-responses',
        plugins: [
            new ExpirationPlugin({
                maxEntries: 50,
                maxAgeSeconds: 5 * 60, // 5 minutes
            }),
        ],
    })
);

// Offline fallback
self.addEventListener('install', (event) => {
    event.waitUntil(
        caches.open('offline-fallback').then((cache) => {
            return cache.add('/offline.html');
        })
    );
});

Push Notifications

// src/utils/pushNotifications.ts
export const requestNotificationPermission = async (): Promise<string | null> => {
    if (!('Notification' in window)) {
        return null;
    }

    const permission = await Notification.requestPermission();

    if (permission === 'granted') {
        const registration = await navigator.serviceWorker.ready;
        const subscription = await registration.pushManager.subscribe({
            userVisibleOnly: true,
            applicationServerKey: process.env.REACT_APP_VAPID_KEY,
        });

        await fetch('/rest/V1/pushnotification/subscribe', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(subscription),
        });

        return JSON.stringify(subscription);
    }

    return null;
};

Apollo Client Cache Configuration

// src/graphql/client.ts
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';

const httpLink = createHttpLink({
    uri: process.env.REACT_APP_GRAPHQL_ENDPOINT,
});

const authLink = setContext((_, { headers }) => {
    const token = localStorage.getItem('auth_token');
    return {
        headers: {
            ...headers,
            authorization: token ? `Bearer ${token}` : '',
        },
    };
});

export const client = new ApolloClient({
    link: authLink.concat(httpLink),
    cache: new InMemoryCache({
        typePolicies: {
            Query: {
                fields: {
                    products: {
                        keyArgs: ['search', 'filter', 'sort'],
                        merge(existing, incoming) {
                            return incoming;
                        },
                    },
                },
            },
        },
    }),
});

Service workers cache static assets and GraphQL responses. Apollo Client manages the client-side data cache with type policies.

Business Context, Architecture, and Production Considerations

Business Requirements Context

Who is the Customer?

Headless Magento targets organizations that need front-end flexibility beyond what Luma/Hyva themes provide:

  • Omnichannel retailers needing consistent UX across web, mobile app, kiosk, and in-store terminals
  • Brands investing in custom UX where design agency deliverables must be implemented without Magento theme constraints
  • Marketplace operators requiring micro-frontend architecture for independent team deployments
  • International retailers needing multi-locale, multi-currency storefronts with region-specific checkout flows
  • Mobile-first businesses where PWA performance on 3G networks is critical for conversion

What Problem Does This Solve?

  1. Frontend velocity: Decoupled frontend deploys independently; no Magento static deployment bottleneck (reduces deploy time from 15-30 min to < 2 min)
  2. Performance: React SSR/SSG achieves LCP < 1.5s vs 3-5s for Luma on mobile
  3. Developer experience: TypeScript, hot reload, modern tooling attracts better talent (reduces hiring difficulty by 40-60%)
  4. Multi-channel: Same GraphQL API powers web, mobile app, and in-store POS
  5. Scalability: Frontend scales on CDN/serverless independently of Magento PHP backend

Architecture Decisions

Decision 1: React + Next.js vs Vue + Nuxt vs Vanilla PWA

Alternative Pros Cons Decision
React + Next.js SSR/SSG, huge ecosystem, TypeScript native Larger bundle, React learning curve Selected - best SSR support, largest talent pool
Vue + Nuxt Smaller bundle, simpler API Smaller ecosystem, fewer Magento integrations Runner-up
Magento PWA Studio (Venia) Official support, built-in Magento patterns Limited customization, small community, abandoned Rejected
Vanilla PWA + Workbox Minimal dependencies No SSR, manual routing, poor DX Rejected

Decision 2: GraphQL vs REST for API Layer

Alternative Pros Cons Decision
GraphQL Single endpoint, precise data fetching, typed schema Query complexity, N+1 risk, caching complexity Selected - fewer round trips, client controls data shape
REST Simpler, better HTTP caching, wider tooling Over/under-fetching, multiple endpoints Rejected
Hybrid (GraphQL + REST) Best of both Maintenance overhead, inconsistent patterns Rejected

Decision 3: Deployment Architecture

Alternative Pros Cons Decision
Vercel/Netlify (serverless) Zero ops, auto-scaling, preview deploys Vendor lock-in, cold starts Selected for MVP
Self-hosted Node.js on Kubernetes Full control, no vendor lock-in Ops burden, team needs K8s expertise Future option
Docker on single VPS Cheap, simple Single point of failure, manual scaling Rejected

Decision 4: State Management

Alternative Pros Cons Decision
Apollo Client cache Built into GraphQL layer, normalized cache Learning curve, cache invalidation complexity Selected - avoids extra dependency
Redux Toolkit Predictable, large ecosystem Separate from GraphQL, boilerplate Rejected
Zustand/Jotai Lightweight, simple No built-in GraphQL integration Rejected

Production Deployment Checklist

Pre-Deployment

  • Run full E2E test suite (Cypress/Playwright)
  • Verify all GraphQL queries pass schema validation
  • Test SSR renders correctly (no hydration mismatches)
  • Verify service worker registration and caching
  • Test PWA installability on iOS and Android
  • Load test frontend at 3x expected traffic
  • Verify CSP headers allow all required domains
  • Test checkout flow end-to-end with real payment
  • Verify Magento GraphQL endpoint rate limiting is configured
  • Test offline fallback page loads correctly

Deployment

  • Deploy frontend to CDN/serverless (zero-downtime)
  • Deploy backend changes during maintenance window
  • Verify GraphQL endpoint is accessible from frontend domain
  • Clear Apollo Client CDN cache if schema changed
  • Update service worker version (cache busting)
  • Run smoke tests against production frontend

Post-Deployment

  • Monitor frontend performance (LCP, FID, CLS)
  • Check GraphQL query latency in Magento
  • Verify cart creation and checkout flow
  • Monitor error rates in Sentry/LogRocket
  • Check push notification delivery
  • Validate SEO metadata renders in SSR

Monitoring and Alerting

Frontend Metrics (Vercel Analytics / Web Vitals)

LCP (Largest Contentful Paint)     → Target < 2.5s
FID (First Input Delay)            → Target < 100ms
CLS (Cumulative Layout Shift)      → Target < 0.1
TTFB (Time to First Byte)          → Target < 200ms
GraphQL Query Duration (p95)       → Target < 500ms

Backend Metrics (Magento + GraphQL)

GraphQL Query Response Time         → Grafana dashboard
MySQL Query Time for product/catalog → slow_query_log
Redis Cache Hit Ratio               → Target > 90%
Elasticsearch Query Latency         → Target < 100ms

Alerting Thresholds

Metric Warning Critical Action
Frontend LCP > 2.5s > 4.0s Check CDN, optimize images
GraphQL latency (p95) > 500ms > 2s Check Magento DB, Redis
Cart creation failure rate > 0.5% > 2% Check Magento API, Redis
Checkout completion rate < 40% < 25% Investigate UX, payment issues
Service worker error rate > 1% > 5% Redeploy SW, check cache
Frontend bundle size > 250KB gzipped > 400KB Analyze bundle, code split

Cost Estimation

Development Cost

Item Hours Rate Cost
React storefront setup + routing 24 $175/hr $4,200
GraphQL queries/mutations (product, cart, checkout) 40 $175/hr $7,000
Authentication + cart management 16 $175/hr $2,800
PWA setup (service worker, manifest) 12 $175/hr $2,100
Testing (E2E + performance) 16 $175/hr $2,800
Total Development 108 $18,900

Infrastructure Cost (Monthly)

Component Cost
Vercel Pro (frontend hosting) $20/mo
Vercel Analytics $10/mo
Sentry (error tracking) $26/mo
Magento hosting (existing) No change
CDN (Vercel Edge Network) Included
Total Monthly ~$56/mo

Annual TCO Comparison

Item Headless Traditional Luma
Development (Year 1) $18,900 $12,000
Infrastructure (annual) $672 $1,200 (Varnish + Redis tuning)
Maintenance (annual) $10,000 $15,000 (theme conflicts, upgrades)
Year 1 Total $29,572 $28,200
Year 2 Total $10,672 $16,200

Headless has higher initial cost but lower long-term maintenance. Break-even occurs in month 14-18.

Quiz

1. What library handles GraphQL in React?

Question 1 options

2. How do you handle cart persistence in headless?

Question 2 options

3. What is the benefit of PWA for headless commerce?

Question 3 options

4. Why was React + Next.js chosen over PWA Studio for headless Magento?

Question 4 options

5. What is the primary business benefit of decoupling frontend from backend?

Question 5 options

6. What TTFB target should a headless Magento storefront aim for?

Question 6 options

Flashcards

Question

What is headless Magento?

Answer

Backend API only, frontend is separate React/Vue app

Question

How does cart work in headless?

Answer

Create cart via GraphQL, store ID in localStorage

Question

What is Apollo Client?

Answer

GraphQL client with caching, error handling, state management

Question

How do you handle authentication?

Answer

Bearer token from GraphQL, stored in localStorage

Question

What is PWA?

Answer

Progressive Web App with offline support and installability

Question

Why React over PWA Studio?

Answer

PWA Studio has limited customization; React offers SSR, larger ecosystem, better talent pool

Question

What is the main deployment advantage?

Answer

Frontend deploys independently in < 2 min via CDN/serverless

Question

What TTFB target should headless aim for?

Answer

< 200ms for fast mobile experience

Question

What is the break-even point for headless vs traditional?

Answer

Month 14-18, higher initial cost but lower long-term maintenance

Revision Notes

Key Takeaways

  • 1. Headless Magento separates frontend from backend
  • 2. Apollo Client manages GraphQL queries and cache
  • 3. Cart is created via GraphQL and persisted via localStorage
  • 4. PWA provides offline support and native app experience
  • 5. Service workers cache API responses and static assets
  • 6. React + Next.js preferred for SSR, ecosystem, and talent pool
  • 7. GraphQL reduces over-fetching compared to REST
  • 8. Frontend deploys independently in < 2 min
  • 9. Headless breaks even vs traditional at month 14-18

Interview Tips

  • Explain headless vs traditional Magento architecture
  • Describe Apollo Client caching strategies
  • Discuss PWA benefits for e-commerce
  • Talk about handling authentication in SPAs
  • Compare React + Next.js vs PWA Studio vs Vue + Nuxt
  • Discuss TCO trade-offs between headless and traditional

Cheat Sheet

Headless Magento:
  Backend: GraphQL API endpoint
  Frontend: React/Next.js app
  Communication: Apollo Client

Cart Flow:
  createEmptyCart → cartId
  addProductsToCart → cart data
  setShippingAddress → shipping methods
  placeOrder → order confirmation

Apollo Client:
  InMemoryCache → Query caching
  useQuery → Fetch data
  useMutation → Write data
  authLink → Add Bearer token

PWA Features:
  Service Worker → Offline caching
  Push Notifications → Order updates
  Web App Manifest → Installability
  Cache First → Static assets

Architecture Decisions:
  React + Next.js: SSR, ecosystem, talent pool
  GraphQL: precise data fetching, single endpoint
  Vercel/serverless: zero ops, auto-scaling
  Apollo cache: avoids Redux dependency

Production:
  Deploy: CDN/serverless (zero-downtime)
  SW: version bump for cache busting
  Monitor: LCP < 2.5s, TTFB < 200ms

Cost:
  Dev: ~$18,900 (108 hours)
  Monthly: ~$56
  Break-even: month 14-18 vs traditional