Shopify
Headless Shopify + Next.js: Storefront API Integration
Fetch products and collections from Shopify Storefront API in Next.js with typed queries, cart handling, and performance patterns.
A headless Shopify storefront on Next.js gives you full control over UX, Core Web Vitals, and merchandising—while Shopify handles inventory, checkout, and payments. The Storefront API (GraphQL) is the integration surface. This article covers client setup, product queries, cart creation, and the decisions that keep the stack production-ready.
Storefront API vs Admin API
- Storefront API — public-facing reads, cart mutations, checkout URLs. Safe to use from the server; expose only the public storefront token via server actions or route handlers, not in client bundles unless you accept token rotation discipline.
- Admin API — orders, products admin, webhooks. Server-only, private app credentials.
For a marketing + commerce site, almost all page rendering uses Storefront API. Admin API is for sync jobs, webhooks, or internal tools.
GraphQL client
// lib/shopify.ts
const domain = process.env.SHOPIFY_STORE_DOMAIN!;
const storefrontToken = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!;
const apiVersion = process.env.SHOPIFY_API_VERSION ?? "2024-10";
export async function shopifyFetch<T>({
query,
variables,
cache = "force-cache",
tags,
}: {
query: string;
variables?: Record<string, unknown>;
cache?: RequestCache;
tags?: string[];
}): Promise<T> {
const res = await fetch(`https://${domain}/api/${apiVersion}/graphql.json`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Shopify-Storefront-Access-Token": storefrontToken,
},
body: JSON.stringify({ query, variables }),
cache,
next: tags ? { tags } : undefined,
});
const json = await res.json();
if (json.errors) {
throw new Error(json.errors[0]?.message ?? "Shopify GraphQL error");
}
return json.data;
}Centralizing fetch here makes it easy to add logging, retry on 429, and swap cache strategy per route.
Product by handle
Handles are stable URLs (/products/classic-tee). Query by handle, not numeric ID, in the storefront.
# lib/shopify-queries.ts
export const PRODUCT_BY_HANDLE = `
query ProductByHandle($handle: String!) {
product(handle: $handle) {
id
title
description
handle
availableForSale
priceRange {
minVariantPrice { amount currencyCode }
}
images(first: 8) {
edges {
node {
url
altText
width
height
}
}
}
variants(first: 20) {
edges {
node {
id
title
availableForSale
selectedOptions { name value }
price { amount currencyCode }
}
}
}
}
}
`;// lib/shopify-products.ts
import { shopifyFetch } from "./shopify";
import { PRODUCT_BY_HANDLE } from "./shopify-queries";
type ProductByHandleData = {
product: {
id: string;
title: string;
handle: string;
// ...narrow in production
} | null;
};
export async function getProduct(handle: string) {
const data = await shopifyFetch<ProductByHandleData>({
query: PRODUCT_BY_HANDLE,
variables: { handle },
tags: [`product:${handle}`],
});
return data.product;
}Map GraphQL edges/node to flat arrays in a mapper so React components do not repeat connection boilerplate.
Collection pages and pagination
Collections use collection(handle:) with products(first: 24, after: $cursor) for cursor pagination. Generate static params for top collections at build time; use dynamicParams = true for long tails.
For SEO, each product and collection page needs unique generateMetadata from Shopify seo fields when you enable them in the admin, or fall back to title and description.
Cart and checkout
Headless flow:
cartCreatemutation → storecart.idin cookiecartLinesAdd/cartLinesUpdatefor mutations- Redirect to
cart.checkoutUrlfor Shopify-hosted checkout
Cart mutations belong in Server Actions or API routes so the storefront token stays off the client if you prefer. Alternatively, use Shopify’s Hydrogen cart utilities if you adopt that stack—same API underneath.
mutation CartCreate {
cartCreate {
cart {
id
checkoutUrl
}
}
}Do not rebuild checkout—Shopify’s hosted checkout handles tax, shipping, and PCI. Your job is fast product discovery and a frictionless path to checkoutUrl.
Performance
- Use
next/imagewith Shopify CDN URLs (already optimized; setsizescorrectly on PLP grids). - Server-render product HTML for crawlers; avoid client-only product shells.
- Tag revalidation: Shopify webhooks (
products/update,collections/update) →/api/revalidatewith product/collection tags. - Limit variant counts in PLP cards; fetch full variant matrix only on PDP.
Theme vs headless
Native Shopify themes are faster to launch for simple DTC stores. Headless wins when you need a custom design system, content from a separate CMS, or sub-second interactions on large catalogs. Budget for cart, webhooks, and SEO parity—not just product pages.
Summary
Headless Shopify on Next.js means a single Storefront GraphQL client, handle-based routing, server-side cart mutations, and checkout via checkoutUrl. Map connection types to plain arrays, cache with tags, revalidate on webhooks, and keep Admin API credentials off the storefront path.