HEADLESS · STOREFRONT API · THEME · 28 APRIL 2026 · 8 MIN READ
Headless for one template, theme for the rest
Partial headless is a legitimate destination, but it is three different architectures wearing one name — and only one of them is cheap. The cart decides which you can have.
Yes, and for most stores asking the question it is the right answer rather than a compromise. But "partial headless" describes three architectures with very different costs: a JavaScript island inside a Liquid template that talks to the Storefront API; an app-proxy route that serves your own rendered HTML under the shop's own domain; and a genuinely separate front end serving a route group, split at the CDN. The thing that decides which you can afford is not rendering — it is the cart. Shopify documents two separate cart systems with separate identifiers, and no mechanism for handing one to the other, so a partial build either keeps one cart everywhere or accepts a one-way handoff at a point you choose deliberately.
IN SHORT
- Partial headless means three different things: an island inside a template, an app-proxy route on the shop domain, or a separate front end for a route group.
- The Ajax Cart API operates on the online store session cart and the Storefront API operates on its own `Cart` object with its own id; neither reference documents a way to share state between them.
- Shopify documents a cart permalink as creating a new checkout for each customer, so it is a handoff, not a merge — anything already in the other cart is not carried across.
- App proxies serve your app under the shop domain at one of four prefixes — `a`, `apps`, `community` or `tools` — and Shopify strips `Set-Cookie` from the response, so you cannot hold your own session there.
- An app proxy response sent as `Content-Type: application/liquid` is rendered as Liquid inside the store theme, which is how one custom route keeps the site header, footer and cart.
- Checkout stays on Shopify in every version of this. The Storefront API `Cart` object exposes `checkoutUrl` for exactly that handoff.
- Choose the boundary at a URL prefix, never at a component, or you own two front ends on the same page.
Three architectures, one name
When a client says they want to go headless for one template, they almost never mean the same thing as the last client who said it. It is worth separating the options before pricing any of them, because the gap between the cheapest and the most expensive is roughly an order of magnitude.
An island inside a Liquid template. The page is still rendered by the theme. A <div> somewhere in it is taken over by a JavaScript component that queries the Storefront API directly from the browser and renders whatever the theme could not — a configurator, a live availability grid, a multi-step builder. This is not headless at all in any architectural sense, and it is the version most people actually want. The theme editor still works, the header and footer are the site's, the cart is the site's cart, and if the component turns out to be a mistake you delete a section file.
An app-proxy route. Your application renders a route, and Shopify serves it under the store's own domain. This is a genuine second rendering system, but it lives on one URL prefix and behind the same hostname as everything else, which removes most of the cross-domain problems that make partial builds painful.
A separate front end for a route group. /products/* on the theme and /collections/* on your own application, or the reverse, split by a rule at the CDN in front of both. This is real headless, applied to a subset. It buys you the most and costs the most, and it is the only one of the three that requires you to answer the cart question up front rather than discovering it in week six.
The cart is the boundary, not the renderer
Here is the constraint that decides the architecture, and it is the one that usually surfaces too late.
Shopify's Ajax Cart API operates on the buyer's session cart on the online store, identified by a token that the reference shows in its responses. The Storefront API operates on a Cart object with its own globally-unique id. They are two systems. Neither reference documents a way to hand a cart from one to the other, and treating them as interchangeable is where partial builds go wrong: a buyer adds three items on a theme-rendered collection page, walks into your headless product route, and the cart there is empty.
The documented escape hatch is a handoff rather than a merge. A cart permalink of the form /cart/{variant_id}:{quantity} pre-populates a cart from a URL, and Shopify's own documentation is explicit that the same link "creates a new checkout for each customer" — new, not merged. So you can push a known set of line items from one side to the other, and you cannot reconcile two carts a buyer has been filling in parallel. Design for the first and you are fine. Assume the second and you will ship a bug that only appears for buyers who browse both halves of the site.
The practical rule: one cart owns the site. If the theme owns it, your headless routes add to the theme cart through the Ajax API from the browser and never build a Storefront API cart at all. If your application owns it, every add-to-cart on the site has to route through it, which in practice means the theme half is reduced to content pages. The first is nearly always the right call for a partial build, and it is also the one that keeps every cart-aware app working.
Checkout is not part of this argument. It stays on Shopify in all three architectures — the Cart object exposes checkoutUrl precisely so a custom front end can hand over, and there is no version of this where you gain by not using it.
What an app proxy actually gives you, and what it takes away
The middle option is the least understood, so it is worth being specific about the documented behaviour, because three details of it decide whether it fits.
You get the shop's domain, on a fixed set of prefixes. Shopify permits four: a, apps, community and tools. A request to https://shop.example/apps/<subpath> is proxied to your application, and child routes proxy too. You do not get /products/custom-thing — you get something under one of those four prefixes, which is fine for a tool, a portal or a configurator and wrong for anything that needs to sit in the catalogue's URL structure.
You get the theme around your content, if you want it. A response returned as Content-Type: application/liquid is rendered as Liquid within the store theme. This is the feature that makes app proxies genuinely useful for partial headless rather than just for webhooks with a UI: your application does the hard rendering, and the page still arrives wrapped in the site's header, navigation, cart drawer and footer, with no duplicate chrome to maintain.
You do not get cookies. Shopify strips Set-Cookie from app proxy responses along with a list of other headers. You cannot hold your own session on a proxied route. What you get instead is a logged_in_customer_id parameter appended to the request when a customer is signed in, plus shop. For a logged-in tool that is often enough; for anything that wanted its own auth, it is a wall, and the workaround people reach for — a second hostname in an iframe — undoes the reason they chose a proxy.
One operational footnote worth knowing before you commit: Shopify documents that changing the proxy url takes effect immediately across all stores, but changes to prefix and subpath apply only to new installations. Pick the path carefully the first time.
If you do split at the CDN
The third architecture is the one that earns the "headless" label, and it is worth doing only when a specific route group is genuinely constrained by Liquid — not when the site as a whole feels slow or hard to edit, both of which have direct fixes.
Split at a URL prefix, and make the prefix boring. A rule that says "everything under /build/* goes to the application, everything else to the theme" is one line at the edge, is trivially reversible, and can be explained to a marketer. A rule that depends on device, cookie or template type is a rule that will eventually serve the wrong front end to Googlebot.
- Keep one hostname. Two front ends on one domain is a routing rule; two domains is a session, analytics and SEO problem you did not have before.
- Duplicate the chrome once, deliberately. Header and footer now exist in two codebases, and the day they drift is the day the split becomes visible to buyers.
- Re-emit the structured data, canonicals and pagination signals by hand on the headless routes. Liquid gave you those; your application does not.
- Audit every app that injects a script into the theme. On the headless routes there is no theme to inject into, so each one is present on half your site or none of it.
- Set a rollback: the route rule reverts to the theme in one change. If it cannot, you have not built a partial migration, you have built a cutover with extra steps.
What we would actually recommend
In most conversations that start with "can we go headless for just the product page", the honest answer is that the product page is not the problem and an island would solve the thing being complained about for a tenth of the cost. Configurators, availability calendars, multi-step builders and live pricing tools all run perfectly well as components inside a Liquid template, hitting the Storefront API from the browser, using the theme's cart. We have shipped that shape more than any other, and nobody has yet asked to replace it with a route split.
The cases where a real split earns its keep are narrower and easy to name: a route group whose rendering genuinely cannot be expressed in Liquid, a front end that must be shared with a native app, or a storefront that is one surface inside a larger application. If yours is one of those, do it as a partial build rather than a rebuild, because a partial build is a decision you can reverse and a rebuild is not.
And whichever you pick, answer the cart question in writing before anyone opens an editor. It is the only part of this that cannot be fixed later without a migration.
Questions this raises
Can a headless route and a Liquid theme share the same cart?
Not as one object. The Ajax Cart API works on the online store session cart and the Storefront API works on its own `Cart` with its own id, and neither reference documents a way to hand one to the other. The workable pattern is for one of them to own the cart everywhere — usually the theme — with your custom routes adding to it rather than building a second one.
Can I serve a custom front end at /products on my Shopify domain?
Not through Shopify itself. App proxies are limited to the `a`, `apps`, `community` and `tools` prefixes, so anything inside the catalogue's own URL structure needs a routing rule at a CDN in front of both systems, which you operate rather than Shopify.
Does a partially headless site lose the theme editor?
Only on the routes you take over. That is the main argument for keeping the split at a URL prefix and keeping it small: merchandisers carry on working normally everywhere else, which is not true of a full migration.
Is an app proxy route good for SEO?
It is on the shop's own domain and can be rendered inside the theme via an `application/liquid` response, so it inherits the site's navigation and internal linking. What it does not inherit is the metadata Liquid templates produce for you — titles, canonicals and structured data are yours to emit.
Can I keep my own user session on an app proxy route?
No. Shopify strips `Set-Cookie` from app proxy responses. You receive `logged_in_customer_id` when a customer is signed in, which is enough for a personalised tool, but not for an application that wanted to run its own authentication.
Is partial headless a migration step or a destination?
Usually a destination, and it should be planned as one. Stores that treat it as phase one of a full rebuild tend to stall after the first route group, having paid for two front ends. Stores that scope it as the permanent shape — one constrained route group on a custom front end, everything else on Liquid — get the benefit without the ongoing bill.
NEXT STEP
Free store audit
A senior Shopify engineer reviews your storefront, theme performance and checkout, then sends a prioritised list of fixes.
