Going Headless
Headless keeps Magento for catalog, cart and orders while another application renders the storefront.
mutation { placeOrder(input: { cart_id: String! }) { orderV2 { number } errors { code message } } }Headless means Magento answers data requests and renders no pages. Another application — React, Vue, a native app, a kiosk — owns the HTML. Magento exposes two APIs for this: REST under /rest and GraphQL at /graphql. GraphQL is the storefront API; REST covers admin and integration work that GraphQL does not.
What you reimplement
Everything the theme used to give you. Layout XML, templates and widgets do not run. CMS pages and Page Builder content come back as an HTML string in cmsPage.content, which you render and style yourself. URL rewrites move to the route query, which takes a path and tells you what it is and whether it redirects. Transactional emails stay server-side and keep using Magento's templates. Varnish still caches pages, but your storefront never asks for one, so full page cache only helps when queries are sent as GET.
The checkout question
This decides the project. Magento's checkout is a Knockout application, and most payment integrations ship only that frontend. GraphQL has the mutations — set the address, set the shipping method, set the payment method, place the order — but a redirecting or hosted-fields provider needs a frontend written per provider, against that provider's SDK. So either budget that work for every active method, or run a hybrid: a headless catalog with Magento rendering the checkout pages.
Keeping it working
Schema and data changes still need their commands: bin/magento setup:upgrade, then bin/magento cache:flush, and bin/magento indexer:reindex before new catalog data appears in a query.
Example
# POST /graphql — the last three steps of a guest checkout
# Shipping address and shipping method must already be set on the cart.
mutation PlaceGuestOrder($cartId: String!, $email: String!) {
setGuestEmailOnCart(input: { cart_id: $cartId, email: $email }) {
cart {
email
}
}
setPaymentMethodOnCart(
input: { cart_id: $cartId, payment_method: { code: "checkmo" } }
) {
cart {
selected_payment_method {
code
title
}
}
}
placeOrder(input: { cart_id: $cartId }) {
orderV2 {
number
}
errors {
code
message
}
}
}When to use it
- Price the work of putting a Next.js catalog in front of an existing Magento store.
- Decide whether to keep Magento's checkout pages while the catalog goes headless.
- Preserve admin-created 301 redirects in a storefront that no longer uses Magento's router.
- Audit which active payment methods would need a new frontend.
More examples
Resolving a storefront URL
Without a theme, nothing maps a path to a page, so this query is the router. relative_url is the canonical path and redirect_code carries the status, so a 301 the merchant created in the admin still works in the storefront. route replaces the deprecated urlResolver query, and it is one of the queries Magento will cache on a GET.
# GET /graphql — what the storefront calls for any unknown path
{
route(url: "joust-duffle-bag.html") {
__typename
relative_url
redirect_code
type
... on SimpleProduct {
sku
url_key
uid
}
}
}Content the storefront has to render itself
content is a string of HTML produced by the admin editor or by Page Builder, with Magento's own markup and class names in it. A headless storefront must inject and style that markup, which is why merchant content tooling is usually the first thing a headless project loses. Page Builder is bundled in Magento Open Source; its staging and content-targeting extras are Adobe Commerce only.
# GET /graphql
{
cmsPage(identifier: "home") {
title
content_heading
content
page_layout
meta_title
meta_description
}
}A REST call, for the work GraphQL does not cover
Reach for REST when a job has no GraphQL equivalent: admin operations, imports, order management from an ERP. Keep the two separate in your head — a customer token is storefront scope, an admin or integration token is not, and sending the wrong one gets a 401 that looks like a broken endpoint.
# a customer token for storefront-scoped REST calls
curl -s -X POST https://example.com/rest/V1/integration/customer/token \
-H 'Content-Type: application/json' \
-d '{"username":"[email protected]","password":"..."}'
# "eyJraWQiOiIx..."
curl -s https://example.com/rest/V1/customers/me \
-H 'Authorization: Bearer eyJraWQiOiIx...'
# GET /V1/products/:sku is guarded by the admin resource Magento_Catalog::products,
# so it needs an admin or integration token - the customer token is rejected
curl -s -X POST https://example.com/rest/V1/integration/admin/token \
-H 'Content-Type: application/json' \
-d '{"username":"ada.admin","password":"..."}'
# "eyJraWQiOiIy..."
curl -s https://example.com/rest/V1/products/24-MB01 \
-H 'Authorization: Bearer eyJraWQiOiIy...'
Discussion