Build a Headless Store: A Shopify Storefront API Example
Are you feeling constrained by traditional Shopify themes? While the theme store offers variety, it can sometimes lead to design limitations and performance bottlenecks that prevent your brand's unique vision fromshining through. If you're seeking unparalleled flexibility, lightning-fast performance, and a truly custom user experience, it's time to explore a headless architecture.
This article is your comprehensive, step-by-step guide to building a custom e-commerce storefront using the Shopify Storefront API. We'll walk through a practical example using modern web technologies like Next.js and GraphQL, covering both the "why" and the "how."
This guide is designed for developers, tech-savvy merchants, and agencies who want to push the boundaries of what's possible with Shopify. Let's dive in.
Understanding Headless Commerce and the Shopify Storefront API
Before we touch any code, let's establish a solid foundation. Understanding these core concepts is key to grasping the power and potential of a headless architecture.
The "Head" vs. The "Body": A Simple Analogy
At its core, headless commerce means decoupling the frontend presentation layer (the "head") from the backend e-commerce engine (the "body").
Think of your e-commerce store as a restaurant:
- The Body (Shopify Backend): This is the kitchen. It handles all the complex, critical operations: managing inventory, processing payments, calculating taxes, and fulfilling orders. It's the powerful, reliable engine that runs your business.
- The Head (Your Custom Frontend): This is the dining room and the menu. You have complete creative control over the ambiance, the layout, the customer journey, and how the food (your products) is presented.

In a headless setup, the kitchen (Shopify) sends data to your dining room (your website) via an API, but it doesn't dictate how the dining room should look or feel. This separation is what gives you ultimate control.
Key Benefits of a Headless Shopify Architecture
Why go through the effort of building a custom frontend? The benefits are significant and can directly impact your user experience and bottom line.
- Unmatched Performance: By using modern frameworks like Next.js, you can leverage Static Site Generation (SSG) and Server-Side Rendering (SSR). This results in incredibly fast page loads, which directly improves user experience, boosts conversion rates, and is a major ranking factor for Google.
- Total Creative Freedom: You are no longer bound by Liquid templates or theme structures. You can build any user experience you can imagine, tailored precisely to your brand and customers. This is perfect for brands with a strong, unique identity that standard themes can't capture.
- Omnichannel-Ready: A headless architecture is inherently omnichannel. The same Shopify backend can power your main website, a progressive web app (PWA), a native mobile app, IoT devices, or even in-store kiosks. Your product data becomes a centralized source of truth for every sales channel.
- Enhanced Developer Experience: Developers can use the tools and frameworks they know and love, like React, Vue, or Svelte. This leads to faster development cycles, higher-quality code, and makes it easier to hire and retain talent.

Storefront API vs. Admin API: What's the Difference?
Shopify provides two primary APIs, and it's crucial to understand their distinct roles:
- Storefront API (Public): This is the API we'll be using. It's designed for customer-facing actions and is safe to use in a public application (like a web browser). It provides read-only access to products and collections and allows for creating carts and initiating checkouts.
- Admin API (Private): This is a protected, private API for backend operations. It's used for managing orders, updating inventory, running reports, and configuring your store. Its API keys must never be exposed on the client-side.
In short: Use the Storefront API to build the customer experience. Use the Admin API for your internal management tools and server-side processes.
Getting Started: Prerequisites and Configuration
Ready to get your hands dirty? Here’s a checklist of everything you need to set up before writing a single line of code.
1. Set Up Your Shopify Development Store
If you don't have one already, you'll need a Shopify store to pull data from. You can create one for free through the Shopify Partners program, which is perfect for testing and development.
- Go to the Shopify Partners website and sign up for a free account.
- From your Partner Dashboard, click "Stores," then "Add store," and choose "Development store."
- Once your store is created, add some sample products, create a collection or two, and upload product images. This will give us real data to work with.
2. Create a Private App and Generate API Credentials
Next, we need to grant our external application permission to access store data via the Storefront API.
- In your Shopify admin dashboard, go to Apps and sales channels > Develop apps for your store.
- Click Create an app. Give it a descriptive name, like "Next.js Headless Store," and click Create app.
- Navigate to the Configuration tab and find Storefront API integration. Click Configure.
- Grant the necessary permissions. For a typical store, you'll want to check all the `unauthenticated_*` permissions. This allows anyone visiting your site to view products and add them to a cart. A good starting set includes:
unauthenticated_read_product_listings
unauthenticated_read_collection_listings
unauthenticated_write_checkouts
unauthenticated_read_checkouts
- Click Save.
- Now, navigate to the API credentials tab. You will find the two pieces of information you need:
- Storefront API access token: This is your public key.
- API endpoint: It will look like
https://your-store-name.myshopify.com/api/2024-04/graphql.json
.
Keep these two values handy. We'll need them in the next section.
3. Essential Tools for Our Example Project
Our example will use a popular and powerful stack for building headless stores. Make sure you have these installed on your machine:
- Node.js and npm/yarn: The JavaScript runtime environment for our project.
- Next.js: A React framework perfect for headless commerce due to its performance features (SSG/SSR) and developer-friendly file-based routing.
- A GraphQL client: We'll use a simple library like
graphql-request
to make our API calls clean and easy.
Building the Storefront: A Practical Next.js Example
This is the core of our guide. We'll build a simple but functional e-commerce storefront step-by-step.
Step 1: Initialize the Next.js Project and API Client
First, let's create our Next.js application and set up environment variables to securely store our API credentials.
Open your terminal and run:
npx create-next-app@latest shopify-headless-store
cd shopify-headless-store
Next, install our GraphQL client:
npm install graphql-request graphql
Now, create a file in the root of your project named .env.local
and add your Shopify credentials. Remember to replace the placeholder values with your actual credentials.
# .env.local
NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=your-store-name.myshopify.com
NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=your-storefront-api-access-token
Finally, let's create a reusable API client. Create a new folder lib
and inside it, a file named shopify.js
.
// lib/shopify.js
import { GraphQLClient } from 'graphql-request';
const domain = process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN;
const storefrontAccessToken = process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN;
const apiVersion = '2024-04'; // Use a specific API version
const storefrontClient = new GraphQLClient(
`https://${domain}/api/${apiVersion}/graphql.json`,
{
headers: {
'X-Shopify-Storefront-Access-Token': storefrontAccessToken,
},
}
);
// Reusable function to make storefront API calls
export async function storefront(query, variables = {}) {
try {
return await storefrontClient.request(query, variables);
} catch (error) {
console.error(JSON.stringify(error, undefined, 2));
throw new Error('Error fetching data from Shopify');
}
}
Step 2: Fetch and Display All Products
Let's create our homepage, which will display a grid of products. We'll use getStaticProps
to fetch this data at build time for maximum performance.
First, create a new file lib/queries.js
to keep our GraphQL queries organized.
// lib/queries.js
export const GET_ALL_PRODUCTS = `
query GetAllProducts {
products(first: 12) {
edges {
node {
id
title
handle
priceRange {
minVariantPrice {
amount
currencyCode
}
}
images(first: 1) {
edges {
node {
url
altText
}
}
}
}
}
}
}
`;
Now, update your pages/index.js
file to fetch and display the products. For styling, this example assumes you have Tailwind CSS set up, which is the default for `create-next-app`.
// pages/index.js
import { storefront } from '../lib/shopify';
import { GET_ALL_PRODUCTS } from '../lib/queries';
import Link from 'next/link';
import Image from 'next/image'; // Using Next.js Image component for optimization
export default function Home({ products }) {
return (
<div className="container mx-auto px-4">
<h1 className="text-3xl font-bold my-8 text-center">Our Products</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8">
{products.map(({ node }) => (
<Link href={`/products/${node.handle}`} key={node.id}>
<a className="group">
<div className="border rounded-lg overflow-hidden">
<div className="relative w-full h-64">
<Image
src={node.images.edges[0]?.node.url}
alt={node.images.edges[0]?.node.altText || `Image for ${node.title}`}
layout="fill"
objectFit="cover"
className="group-hover:scale-105 transition-transform duration-300"
/>
</div>
<div className="p-4">
<h2 className="text-lg font-semibold truncate">{node.title}</h2>
<p className="text-gray-700">
${parseFloat(node.priceRange.minVariantPrice.amount).toFixed(2)}
</p>
</div>
</div>
</a>
</Link>
))}
</div>
</div>
);
}
export async function getStaticProps() {
const { products } = await storefront(GET_ALL_PRODUCTS);
return {
props: {
products: products.edges,
},
revalidate: 60, // Re-generate the page every 60 seconds (Incremental Static Regeneration)
};
}
Step 3: Create a Dynamic Product Details Page
Next, we need a page to show the details of a single product. We'll use Next.js's dynamic routing by creating a file at pages/products/[handle].js
.
First, add the query to fetch a single product by its handle to lib/queries.js
.
// lib/queries.js (add this to the file)
export const GET_PRODUCT_BY_HANDLE = `
query GetProductByHandle($handle: String!) {
product(handle: $handle) {
id
title
descriptionHtml
priceRange {
minVariantPrice {
amount
currencyCode
}
}
images(first: 5) {
edges {
node {
url
altText
}
}
}
variants(first: 10) {
edges {
node {
id
title
priceV2 {
amount
currencyCode
}
}
}
}
}
}
`;
Now, let's build the dynamic page component in pages/products/[handle].js
.
// pages/products/[handle].js
import { storefront } from '../../lib/shopify';
import { GET_ALL_PRODUCTS, GET_PRODUCT_BY_HANDLE } from '../../lib/queries';
import Image from 'next/image';
export default function ProductPage({ product }) {
// A fallback for when the page is being generated
if (!product) return <div>Loading...</div>;
return (
<div className="container mx-auto p-4 md:p-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<div className="relative w-full h-96 rounded-lg overflow-hidden">
<Image
src={product.images.edges[0]?.node.url}
alt={product.images.edges[0]?.node.altText || product.title}
layout="fill"
objectFit="cover"
/>
</div>
</div>
<div>
<h1 className="text-4xl font-bold mb-4">{product.title}</h1>
<p className="text-2xl text-gray-800 mb-6">
${parseFloat(product.priceRange.minVariantPrice.amount).toFixed(2)}
</p>
<div
className="prose lg:prose-xl mb-6"
dangerouslySetInnerHTML={{ __html: product.descriptionHtml }}
/>
<button className="bg-blue-600 text-white font-bold py-3 px-8 rounded-lg hover:bg-blue-700 transition-colors">
Add to Cart
</button>
</div>
</div>
</div>
);
}
// This function pre-builds all the possible product pages
export async function getStaticPaths() {
const { products } = await storefront(GET_ALL_PRODUCTS);
const paths = products.edges.map(({ node }) => ({
params: { handle: node.handle },
}));
return {
paths,
fallback: 'blocking', // or true, if you have many products
};
}
// This function fetches the data for a single product page
export async function getStaticProps({ params }) {
const { product } = await storefront(GET_PRODUCT_BY_HANDLE, {
handle: params.handle,
});
return {
props: {
product,
},
revalidate: 60,
};
}
Step 4: Implementing the Shopping Cart
The shopping cart involves mutations
—GraphQL operations that change data. The flow is: 1) Create a cart, 2) Store the cart ID locally, and 3) Add items to the cart using its ID.
Create a new file lib/mutations.js
for these operations.
// lib/mutations.js
export const CREATE_CART = `
mutation cartCreate($input: CartInput!) {
cartCreate(input: $input) {
cart {
id
checkoutUrl
}
}
}
`;
export const ADD_TO_CART = `
mutation cartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {
cartLinesAdd(cartId: $cartId, lines: $lines) {
cart {
id
checkoutUrl
lines(first: 10) {
edges {
node {
id
quantity
merchandise {
... on ProductVariant {
id
title
product {
title
}
}
}
}
}
}
}
}
}
`;
Implementing a full cart with state management is complex and beyond a single code snippet, but the core logic for an "Add to Cart" button's `onClick` handler would look something like this. This is a conceptual example to illustrate the API calls.
// Conceptual code for an "Add to Cart" button click handler
async function handleAddToCart() {
let cartId = localStorage.getItem('cartId');
// 1. If no cart exists in local storage, create one
if (!cartId) {
const { cartCreate } = await storefront(CREATE_CART, { input: {} });
cartId = cartCreate.cart.id;
localStorage.setItem('cartId', cartId);
}
// 2. Add the selected variant to the cart
const variables = {
cartId,
lines: [{
merchandiseId: selectedVariantId, // The ID of the chosen product variant
quantity: 1,
}],
};
const { cartLinesAdd } = await storefront(ADD_TO_CART, variables);
// 3. Update UI to show the item is in the cart and store the new cart state
console.log('Item added!', cartLinesAdd.cart);
// In a real app, you would update your state here (e.g., using React Context or Zustand)
}
Step 5: The Secure Checkout Redirect
This is the most important—and easiest—step. You do not build the checkout pages yourself. Shopify handles the entire checkout process to ensure maximum security and PCI compliance.
Your only job is to redirect the user to the correct URL. Notice that every cart mutation response includes a checkoutUrl
. When the user is ready to check out, you just need to grab that URL from your cart state and direct them to it.
// Example of a "Proceed to Checkout" button
function CheckoutButton({ checkoutUrl }) {
return (
<a
href={checkoutUrl}
target="_blank" // Open in a new tab for a better user experience
rel="noopener noreferrer"
className="bg-green-600 text-white font-bold py-3 px-6 rounded-lg hover:bg-green-700"
>
Proceed to Checkout
</a>
);
}
Advanced Topics and Best Practices
Once you have the basics down, consider these topics to take your headless store to the next level.
Optimizing for Performance and SEO
- Image Optimization: Use Next.js's built-in
<Image>
component. It automatically optimizes images, serves them in modern formats like WebP, and prevents layout shift. - Dynamic Metadata: On your product details page (
[handle].js
), use Next.js's<Head>
component to dynamically set the page title and meta description based on the product's data. This is crucial for SEO. - Incremental Static Regeneration (ISR): Using the
revalidate
property ingetStaticProps
allows your static pages to be updated in the background without needing a full site rebuild, keeping your content fresh.
Handling API Rate Limits
Shopify uses a "leaky bucket" algorithm for rate limiting to ensure API stability. To avoid hitting limits:
- Be specific: Only query for the data fields you actually need in your GraphQL queries.
- Cache wisely: Use ISR (
revalidate
) to avoid re-fetching data on every single request for content that doesn't change often. - Avoid N+1 queries: Structure your GraphQL queries to fetch all necessary data in a single request where possible, rather than making multiple requests in a loop.
Exploring Shopify Hydrogen and Oxygen
If you want a more opinionated, all-in-one solution, Shopify has its own headless stack:
- Hydrogen: A React-based framework specifically designed for building headless Shopify stores. It comes with pre-built components and hooks for cart, variants, and more, simplifying development.
- Oxygen: Shopify's global hosting solution, optimized for deploying and running Hydrogen storefronts with a single click.
This stack is a great alternative if you prefer to stay entirely within the Shopify ecosystem for both development and hosting.
Conclusion: Is a Headless Store Right for You?
By going headless with the Shopify Storefront API, you unlock a new level of power, performance, and creative control. You can build faster, more engaging, and truly unique shopping experiences that stand out from the competition.
So, how do you decide if it's the right path?
- Go Headless if: You have a strong brand identity that requires a custom design, you need top-tier performance to maximize conversions, or you're planning an omnichannel strategy across web, mobile, and other platforms.
- Stick with Themes if: You're just starting out, have a limited budget or development team, or your business needs are comfortably met by the existing themes and apps in the Shopify ecosystem.
Building a headless store is a rewarding journey that puts you in the driver's seat. We encourage you to explore the official Shopify Storefront API documentation and start building the future of your brand today.