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?
- Frontend velocity: Decoupled frontend deploys independently; no Magento static deployment bottleneck (reduces deploy time from 15-30 min to < 2 min)
- Performance: React SSR/SSG achieves LCP < 1.5s vs 3-5s for Luma on mobile
- Developer experience: TypeScript, hot reload, modern tooling attracts better talent (reduces hiring difficulty by 40-60%)
- Multi-channel: Same GraphQL API powers web, mobile app, and in-store POS
- 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?
2. How do you handle cart persistence in headless?
3. What is the benefit of PWA for headless commerce?
4. Why was React + Next.js chosen over PWA Studio for headless Magento?
5. What is the primary business benefit of decoupling frontend from backend?
6. What TTFB target should a headless Magento storefront aim for?
Flashcards
Question
What is headless Magento?
Click to reveal answer
Answer
Backend API only, frontend is separate React/Vue app
Question
How does cart work in headless?
Click to reveal answer
Answer
Create cart via GraphQL, store ID in localStorage
Question
What is Apollo Client?
Click to reveal answer
Answer
GraphQL client with caching, error handling, state management
Question
How do you handle authentication?
Click to reveal answer
Answer
Bearer token from GraphQL, stored in localStorage
Question
What is PWA?
Click to reveal answer
Answer
Progressive Web App with offline support and installability
Question
Why React over PWA Studio?
Click to reveal answer
Answer
PWA Studio has limited customization; React offers SSR, larger ecosystem, better talent pool
Question
What is the main deployment advantage?
Click to reveal answer
Answer
Frontend deploys independently in < 2 min via CDN/serverless
Question
What TTFB target should headless aim for?
Click to reveal answer
Answer
< 200ms for fast mobile experience
Question
What is the break-even point for headless vs traditional?
Click to reveal answer
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