Integrating Shopify Storefront API with a custom frontend
The standard Shopify theme hits the ceiling of performance and customization: the URL architecture is rigidly fixed (only /products/product-handle), checkout cannot be customized without a Plus subscription, and complex animations turn into a dance with a tambourine around Liquid. As a result, pages load slowly: TTFB can reach 800 ms, LCP — 4–6 seconds on mobile devices.
When a client asks for a non-standard interface — custom pages, complex filters, PWA, multilingual support — we switch to headless: we use Shopify as a headless commerce backend, and write the frontend in Next.js, Nuxt, or Astro.
Over the past few months, we have delivered more than a dozen such projects — from custom storefronts to PWA applications with search integration and personalized recommendations.
In this article, we will break down how Headless commerce works on Shopify Storefront API: from authentication to cart and ISR. You will get concrete code examples and architectural solutions for your own project.
Problems that headless solves
Limitations of the standard theme:
- URL architecture is inflexible — you can't do
/brand/product-name, which hurts SEO. - Checkout without Shopify Plus can't be customized: fields, steps, custom scenarios are unavailable.
- Animation and UX — Liquid can't handle complex animation, React/Next.js handles it easily.
- Merging multiple stores — one storefront can aggregate products from different Shopify accounts.
- A mobile app uses the same API as the web, reducing development.
Each of these problems costs a business time and money. We solve them with the Storefront API — a GraphQL interface that provides access to the catalog, cart, and checkout.
Hosting savings from static content amount to cost calculated after audit per month, and conversion grows by 20–30% thanks to speed.
How the Storefront API works: authentication
To gain access, you need a public Storefront API access token — it is created in the admin panel: Admin > Apps > Develop apps > [App] > Configuration > Storefront API access scopes
The token is passed in the X-Shopify-Storefront-Access-Token header. It is public, so it is embedded in the frontend JS code — this is safe because the permissions are limited (reading the catalog, cart mutations).
Our TypeScript client looks like this:
// lib/shopify/client.ts
const SHOPIFY_DOMAIN = process.env.SHOPIFY_STORE_DOMAIN!;
const STOREFRONT_TOKEN = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!;
export async function storefrontFetch<T>({
query,
variables,
cache = 'force-cache',
tags,
}: {
query: string;
variables?: Record<string, unknown>;
cache?: RequestCache;
tags?: string[];
}): Promise<T> {
const res = await fetch(
`https://${SHOPIFY_DOMAIN}/api/graphql.json`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Storefront-Access-Token': STOREFRONT_TOKEN,
},
body: JSON.stringify({ query, variables }),
cache,
next: tags ? { tags } : undefined,
}
);
if (!res.ok) throw new Error(`Storefront API error: ${res.status}`);
const { data, errors } = await res.json();
if (errors?.length) throw new Error(errors[0].message);
return data;
}
How to retrieve a product catalog via the Storefront API?
A typical product query with metafields:
// lib/shopify/queries/products.ts
const GET_PRODUCTS = `
query getProducts($first: Int!, $after: String, $sortKey: ProductSortKeys, $reverse: Boolean, $query: String) {
products(first: $first, after: $after, sortKey: $sortKey, reverse: $reverse, query: $query) {
edges {
cursor
node {
id
handle
title
availableForSale
priceRange {
minVariantPrice {
amount
currencyCode
}
maxVariantPrice {
amount
currencyCode
}
}
featuredImage {
url
altText
width
height
}
variants(first: 1) {
edges {
node {
id
availableForSale
selectedOptions {
name
value
}
}
}
}
metafield(namespace: "custom", key: "badge") {
value
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
export async function getProducts({
first = 24,
after,
sortKey = 'RELEVANCE',
reverse = false,
query,
}: ProductsQueryParams) {
const data = await storefrontFetch<{ products: ProductConnection }>({
query: GET_PRODUCTS,
variables: { first, after, sortKey, reverse, query },
tags: ['products'],
});
return data.products;
}
Key features: cursor-based pagination via after, the ability to filter via the query parameter (Shopify Search syntax). For faceted filtering, we use collection.products with filters by price, attributes, and availability.
How to manage the cart via the Cart API?
The modern Cart API replaces the deprecated Checkout API. The cart is stored on the Shopify side; we save the ID in a cookie or localStorage.
// lib/shopify/queries/cart.ts
const CREATE_CART = `
mutation cartCreate($input: CartInput) {
cartCreate(input: $input) {
cart {
id
checkoutUrl
lines(first: 100) {
edges {
node {
id
quantity
merchandise {
... on ProductVariant {
id
title
price {
amount
currencyCode
}
product {
title
featuredImage {
url
altText
}
}
}
}
}
}
}
cost {
subtotalAmount {
amount
currencyCode
}
totalAmount {
amount
currencyCode
}
totalTaxAmount {
amount
currencyCode
}
}
}
userErrors {
field
message
}
}
}
`;
const ADD_TO_CART = `
mutation cartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {
cartLinesAdd(cartId: $cartId, lines: $lines) {
cart {
id
lines(first: 100) {
edges {
node {
id
quantity
}
}
}
}
userErrors {
field
message
}
}
}
`;
export async function addToCart(cartId: string, variantId: string, quantity: number) {
return storefrontFetch({
query: ADD_TO_CART,
variables: {
cartId,
lines: [{ merchandiseId: variantId, quantity }],
},
cache: 'no-store',
});
}
When proceeding to payment, we redirect the user to cart.checkoutUrl — this is Shopify's hosted checkout.
Why is ISR a key feature for a headless store?
Incremental Static Regeneration (ISR) allows product pages to be rendered statically at build time and then updated via a webhook from Shopify or by TTL. This approach delivers the speed of static with the freshness of dynamic.
// app/api/revalidate/route.ts — webhook from Shopify
import { revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(req: NextRequest) {
const hmac = req.headers.get('x-shopify-hmac-sha256');
// HMAC verification...
const body = await req.json();
const topic = req.headers.get('x-shopify-topic');
if (topic === 'products/update' || topic === 'products/create') {
revalidateTag('products');
revalidateTag(`product-${body.handle}`);
}
if (topic === 'collections/update') {
revalidateTag('collections');
}
return new Response('OK');
}
In practice, this means: a product appears in Shopify — a second later it's already on the site. At the same time, the page's HTML is cached on the CDN, and LCP drops to 0.5 seconds.
Internationalization: one storefront for all countries
Storefront API supports the @inContext directive for localizing prices and content:
query getProduct($handle: String!, $country: CountryCode!, $language: LanguageCode!) @inContext(country: $country, language: $language) {
product(handle: $handle) {
title
priceRange {
minVariantPrice {
amount
currencyCode
}
}
}
}
Work process
- Audit and architecture — analyze the existing store, migrate metafields and settings.
- API design — define queries, optimize batch loading.
- Frontend development — build components in Next.js, configure ISR and caching.
- Cart integration — Cart API, cookies, redirect to checkout.
- Testing — verify all purchase scenarios, performance, SEO.
- Deployment — CI/CD with versioning, monitoring.
Timeline approximately
| Stage | Timeline |
|---|---|
| MVP (catalog + cart + checkout) | 3–4 weeks |
| Full project (search, filters, ISR, multilingual) | 2–3 months |
| Support and enhancements | by agreement |
Comparison: standard theme vs headless
| Criterion | Standard Shopify theme | Headless (Next.js + ISR) |
|---|---|---|
| TTFB | 500–800 ms | < 50 ms |
| LCP | 4–6 s | < 1 s |
| URL customization | Only /products/handle | Any patterns |
| Checkout | Only standard (Plus — expensive) | Full control via API |
| Animations | Liquid — limited | React/Svelte — no restrictions |
Common mistakes during migration
- They forget to set up webhooks for product updates — content on the site becomes outdated. - They don't optimize GraphQL queries — they get the N+1 problem. - They use the outdated Checkout API instead of the Cart API.What is included in the work
Documentation — API description, deployment instructions. Access — setting up tokens, webhooks, DNS. Training — consultation for your developers. Warranty — 6 months of free support for critical bugs. Performance audit — Lighthouse score of at least 90.
Why is the headless approach faster than Liquid?
Let's compare: a standard Shopify theme renders the page on the server on every request (TTFB ~500 ms). Headless with ISR serves ready-made HTML from the CDN (TTFB < 50 ms). Animations and interactives run on the client, without reloads. This delivers up to a 3x difference in LCP.
Our team has 8+ years of experience with Shopify, more than 50 headless solution implementations. We use best practices: React Server Components, Suspense, streaming SSR. Contact us to evaluate your project — we'll calculate timelines and cost individually. Get a consultation on headless migration — we'll analyze your store and propose an architecture.







