Shopify Storefront API Integration with Custom Frontend
Shopify Storefront API allows using Shopify as a headless commerce backend — catalog management, cart, and checkout via GraphQL, while the frontend is completely custom: Next.js, Nuxt, Astro, mobile app, or any other client.
When Headless is Needed
Standard Shopify theme has limitations:
- URL structure is rigidly defined by the platform
- Custom checkout unavailable without Shopify Plus
- Complex animations and non-standard interfaces require workarounds in Liquid
- Need a unified storefront for multiple Shopify stores
- PWA or native mobile app
Headless solves these, but adds operational complexity: separate hosting, CI/CD, separate frontend deploy.
Storefront API: Authentication
To access Storefront API, you need a Storefront API access token — public token with limited permissions (catalog read-only and cart mutations):
Admin > Apps > Develop apps > [App] > Configuration > Storefront API access scopes
Token passed in X-Shopify-Storefront-Access-Token header — can be public (embedded in frontend JS).
Basic Client
// 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/2025-01/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;
}
Fetching Catalog
// 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;
}
Cart Management
Storefront API uses Cart API (not deprecated Checkout API):
// 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',
});
}
Cart stored on Shopify, cart ID saved in cookie or localStorage. On checkout, use cart.checkoutUrl — redirect to Shopify checkout.
Next.js App Router Integration
// app/products/[handle]/page.tsx
import { getProduct } from '@/lib/shopify';
import { AddToCartButton } from '@/components/AddToCartButton';
import { notFound } from 'next/navigation';
export async function generateStaticParams() {
const products = await getProducts({ first: 250 });
return products.edges.map(({ node }) => ({ handle: node.handle }));
}
export async function generateMetadata({ params }: { params: { handle: string } }) {
const product = await getProduct(params.handle);
if (!product) return {};
return {
title: product.title,
description: product.description.slice(0, 160),
openGraph: {
images: [{ url: product.featuredImage?.url }],
},
};
}
export default async function ProductPage({ params }: { params: { handle: string } }) {
const product = await getProduct(params.handle);
if (!product) notFound();
return (
<main>
<h1>{product.title}</h1>
<AddToCartButton product={product} />
</main>
);
}
Incremental Static Regeneration (ISR)
Catalog is statically rendered at build, updated via webhook or TTL:
// 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');
}
Search and Filtering
Storefront API supports query parameter with Shopify search syntax:
// Filter by tag + price
const products = await getProducts({
query: 'tag:sale price:<5000',
sortKey: 'PRICE',
reverse: false,
});
// Search by name
const results = await getProducts({
query: `title:*${searchTerm}*`,
});
For complex filtering (attribute facets) — use collection.products with filters:
collection(handle: "all") {
products(first: 24, filters: [
{ price: { min: 1000, max: 5000 } },
{ productMetafield: { namespace: "specifications", key: "material", value: "leather" } },
{ available: true }
]) { ... }
}
Internationalization
Storefront API supports @inContext directive for localization:
query getProduct($handle: String!, $country: CountryCode!, $language: LanguageCode!)
@inContext(country: $country, language: $language) {
product(handle: $handle) {
title
priceRange {
minVariantPrice { amount currencyCode }
}
}
}
Timeline
MVP headless store on Next.js with catalog, cart, checkout: 3–4 weeks. Full project with CMS, multi-market, custom analytics, A/B testing, CI/CD: 2–3 months.







