I wanted my merch store to feel like the rest of my site, not like a Shopify theme with my logo dropped on top. So I built it headless: Shopify runs the commerce, and a custom Next.js frontend runs everything you see. Here is how the pieces fit, and the parts that surprised me.
Why I skipped the Shopify theme
Shopify themes are fine. They stand up fast and handle a lot for you. But you are working inside Liquid and someone else's structure, and the moment I wanted GSAP transitions and my own design system, I was fighting the theme instead of building.
Going headless meant I owned the frontend completely and let Shopify keep doing the part it is genuinely good at: products, inventory, payments, and checkout.
What "headless" actually means here
Headless just means the storefront and the commerce backend are two separate things talking over an API. Shopify holds the products, collections, prices, and orders. My Next.js app asks for that data through Shopify's Storefront API and renders it however I want. Nothing about my UI is Shopify's concern, and nothing about inventory or payments is mine.
The Storefront API
The Storefront API is a GraphQL API built exactly for this. You create a storefront access token in Shopify and query it from your frontend for products, collections, and carts. It is read-oriented for the catalog and gives you cart mutations for the buying flow.
A products query looks about like this:
const QUERY = `
query Products($first: Int!) {
products(first: $first) {
nodes {
id
title
handle
featuredImage { url altText }
priceRange { minVariantPrice { amount currencyCode } }
}
}
}
`
const res = await fetch(`https://${shop}/api/2024-10/graphql.json`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Shopify-Storefront-Access-Token": token,
},
body: JSON.stringify({ query: QUERY, variables: { first: 20 } }),
})The `handle` is the field that matters most. It is Shopify's URL slug for a product, and it becomes the thing my routes, my metadata, and my sitemap all hang off.
Fetching products with TanStack Query
I used TanStack Query for the client-side data. Product lists, a single product, the cart, all of it goes through queries and mutations with a cache in front. A product I already loaded does not refetch when I navigate back to it, and cart updates feel instant because the UI can update optimistically while the mutation runs.
I could have done more of this on the server, and for the catalog pages I did use server rendering so the product content is there for search engines. But the cart is client state that changes as you click around, so TanStack Query earned its place there.
The cart, and why checkout stays on Shopify
This is the part people get wrong when they first go headless. You build the cart, but you do not build the checkout.
Shopify's cart is an object you create and add lines to through the Storefront API. `cartCreate` gives you a cart, `cartLinesAdd` puts items in it, and the cart comes back with a `checkoutUrl`. When the customer is ready to pay, you send them to that URL and Shopify's hosted checkout takes over.
// build the cart yourself, then hand off to Shopify to take the money
window.location.href = cart.checkoutUrlI like this boundary. Payments, taxes, shipping rates, fraud checks, and PCI compliance are Shopify's problem, not mine. I get to obsess over the storefront and leave the regulated part with the people who do it for a living.
Motion: GSAP and Lenis
The reason to go headless was the frontend, so I spent real time on how it moves. GSAP handles clip-path reveals and staggered product lists, so items arrive with intent instead of popping in. Lenis handles smooth scrolling, which sounds like a small thing until you scroll a store that doesn't have it. For a small merch shop, that polish is most of what makes it feel like a real brand instead of a template.
None of it touches the commerce layer. It is all frontend, which is the whole point of keeping the two apart.
SEO: metadata and a sitemap from product handles
If people can't find the products, the store doesn't sell, so I couldn't treat SEO as a last step.
I built a metadata system in Next.js that generates a title and description for every product and every collection straight from the Shopify data, so each page tells search engines what it actually is. Then I generated the sitemap and `robots.txt` automatically by fetching every product handle from Shopify at build time. Add a product in Shopify, rebuild, and it is in the sitemap without me touching anything. The `handle` from earlier is what ties the route, the metadata, and the sitemap entry together.
What I'd tell you before you start
Two things.
Decide what is yours and what is Shopify's early. The clean line is that you own presentation and Shopify owns money. Every time I was tempted to reimplement something Shopify already does, like checkout or tax or shipping, stopping myself was the right call.
And treat the `handle` as your backbone. Routes, metadata, sitemap, and links all key off it, so lean on it as the stable identifier instead of reaching for the numeric ID.
Where it leaves me
Headless Shopify gave me a store that looks and moves like the rest of my work, with Shopify quietly handling the parts I have no business rebuilding. If you want a shop that reads as your brand instead of a stock theme, this is how I'd build it for you.

Devakintola store
My official merch store: a custom headless storefront in Next.js wired to Shopify's Storefront API, with GSAP motion and smooth scrolling.

devakintola
My portfolio site, built as a showcase in itself: a Next.js front end with every content type modelled and managed in a custom Sanity Studio.


