Creating a Headless Storefront with Next.js and Hydrogen

Table of Contents
Big thanks to our contributors those make our blogs possible.

Our growing community of contributors bring their unique insights from around the world to power our blog. 

Introduction

Modern e-commerce demands flexibility, performance, and seamless developer experience. A headless storefront—where the frontend is decoupled from the backend—empowers teams to iterate rapidly, optimize for conversions, and deliver lightning-fast user interfaces. In this guide, we’ll explore how to build a headless storefront using Next.js for your presentation layer and Shopify’s Hydrogen framework as a React-based toolkit for commerce primitives. You’ll learn how to architect your project, fetch product data via GraphQL, design dynamic pages, and optimize for performance and SEO. Whether you’re migrating from a monolith or starting fresh, by the end, you’ll have a clear roadmap and code samples to launch a scalable, future-proof storefront.

Main Body

1. Understanding Headless Commerce

Headless commerce separates the backend (product catalog, inventory, checkout) from the frontend (UI, routing, presentation) via APIs.

1.1 Benefits of Going Headless

  • Developer Agility: Frontend teams choose frameworks and libraries without backend constraints.
  • Omnichannel Delivery: Power websites, mobile apps, kiosks, and IoT devices from the same backend.
  • Performance & SEO: Static-generation (SSG) and incremental static regeneration (ISR) deliver fast, crawlable pages.
  • Scalability: Independently scale frontend and backend based on demand.

1.2 Why Next.js + Hydrogen?

  • Next.js: Industry-leading React framework with SSG, SSR, API routes, and image optimizations.
  • Hydrogen: Shopify’s React library offering commerce hooks (useShopQuery), cart state management, and UI components designed for GraphQL storefront APIs.

Analogy: Think of Next.js as the engine and chassis of a car—fast, reliable, and feature-rich. Hydrogen is the custom suspension and braking system tailored for e-commerce, ensuring you handle commerce data smoothly and securely.

2. Project Architecture & File Structure

A clear architecture keeps your code maintainable as your catalog grows.

plaintextCopyEditmy-storefront/
├── public/                 # Static assets (images, favicon)
├── src/
│   ├── components/         # Reusable UI (ProductCard, Cart)
│   ├── hooks/              # Custom hooks (useCart, useShop)
│   ├── pages/              # Next.js pages (index.js, [handle].js)
│   ├── lib/                # Shopify GraphQL client setup
│   ├── styles/             # Global and module CSS
│   └── utils/              # Helpers (formatPrice, getImageUrl)
├── .env.local              # API keys and secrets
├── next.config.js          # Next.js configuration
├── package.json            
└── hydrogen.config.js      # Hydrogen-specific settings
  • pages/: Next.js file-based routing drives dynamic product and collection pages.
  • components/: Encapsulate UI elements for reuse and faster iteration.
  • lib/: Initialize Shopify’s GraphQL client once, so all queries share configuration.

3. Setting Up Your Environment

3.1 Install Dependencies

bashCopyEditnpm init next-app my-storefront
cd my-storefront

# Install Next.js, React and Hydrogen packages
npm install react react-dom next
npm install @shopify/hydrogen @shopify/storefront-kit graphql

3.2 Configure Environment Variables

Create a .env.local file in your project root:

envCopyEditSHOPIFY_STORE_DOMAIN=your-shop.myshopify.com
SHOPIFY_STOREFRONT_API_TOKEN=your_public_token

Load them in next.config.js:

jsCopyEditmodule.exports = {
  env: {
    SHOPIFY_STORE_DOMAIN: process.env.SHOPIFY_STORE_DOMAIN,
    SHOPIFY_STOREFRONT_API_TOKEN: process.env.SHOPIFY_STOREFRONT_API_TOKEN,
  },
  images: {
    domains: ['cdn.shopify.com'],
  },
};

4. Initializing the Shopify Client

In src/lib/shopify.js:

jsCopyEditimport { ShopifyGraphQLClient } from '@shopify/hydrogen';

export const storefrontClient = new ShopifyGraphQLClient({
  storeDomain: process.env.SHOPIFY_STORE_DOMAIN,
  token: process.env.SHOPIFY_STOREFRONT_API_TOKEN,
});

Create a helper to run queries:

jsCopyEditexport async function shopifyQuery(query, variables = {}) {
  const response = await storefrontClient.query({
    data: { query, variables },
  });
  if (response.errors) throw new Error(response.errors[0].message);
  return response.data;
}

Expert Insight: Centralizing your client ensures consistent headers, error handling, and caching strategies across the app.

5. Building Dynamic Product Pages

5.1 GraphQL Query for Product Data

Define a query in src/queries/getProductByHandle.js:

graphqlCopyEditquery ProductByHandle($handle: String!) {
  productByHandle(handle: $handle) {
    id
    title
    description
    images(first: 5) {
      edges {
        node {
          url
          altText
        }
      }
    }
    variants(first: 10) {
      edges {
        node {
          id
          title
          priceV2 {
            amount
            currencyCode
          }
        }
      }
    }
  }
}

5.2 Next.js Dynamic Route

In src/pages/products/[handle].js:

jsxCopyEditimport React from 'react';
import { shopifyQuery } from '../../lib/shopify';
import PRODUCT_QUERY from '../../queries/getProductByHandle';

export async function getStaticPaths() {
  // Fetch list of product handles
  const { products } = await shopifyQuery(`{ products(first: 50) { edges { node { handle } } } }`);
  const paths = products.edges.map(({ node }) => ({ params: { handle: node.handle } }));
  return { paths, fallback: 'blocking' };
}

export async function getStaticProps({ params }) {
  const data = await shopifyQuery(PRODUCT_QUERY, { handle: params.handle });
  return { props: { product: data.productByHandle }, revalidate: 60 };
}

export default function ProductPage({ product }) {
  return (
    <div>
      <h1>{product.title}</h1>
      <p>{product.description}</p>
      {/* Map images, render variants, add AddToCart button */}
    </div>
  );
}
  • getStaticPaths: Generates product pages at build time.
  • getStaticProps + ISR: Revalidates stale pages every 60 seconds for up-to-date inventory.

6. Managing Cart State with Hydrogen Hooks

Hydrogen exposes a cart hook for easy cart management.

6.1 Creating a Cart Provider

In src/hooks/useCart.js:

jsxCopyEditimport { createContext, useContext } from 'react';
import { CartProvider as HydrogenCartProvider, useCart } from '@shopify/hydrogen';

const CartContext = createContext();

export function CartProvider({ children }) {
  const cart = useCart(); // Hydrogen hook

  return <CartContext.Provider value={cart}>{children}</CartContext.Provider>;
}

export function useCartContext() {
  return useContext(CartContext);
}

Wrap your app in _app.js:

jsxCopyEditimport { CartProvider } from '../hooks/useCart';

export default function App({ Component, pageProps }) {
  return (
    <CartProvider>
      <Component {...pageProps} />
    </CartProvider>
  );
}

6.2 Adding to Cart

In your ProductPage component:

jsxCopyEditimport { useCartContext } from '../../hooks/useCart';

function AddToCartButton({ variantId }) {
  const { linesAdd } = useCartContext();

  return (
    <button onClick={() => linesAdd([{ merchandiseId: variantId, quantity: 1 }])}>
      Add to Cart
    </button>
  );
}

Tip: Hydrogen’s cart state persists via cookies—no extra setup needed.

7. Enhancing Performance & SEO

7.1 Image Optimization

  • Use Next.js <Image> for lazy loading, responsive sizes, and WebP support.
  • Leverage Shopify’s width and format URL parameters for on-the-fly resizing.
jsxCopyEditimport Image from 'next/image';

<Image
  src={image.url}
  alt={image.altText}
  width={800}
  height={800}
  priority={index === 0}
/>

7.2 Head Tags & Structured Data

In your ProductPage:

jsxCopyEditimport Head from 'next/head';

<Head>
  <title>{product.title} | My Store</title>
  <meta name="description" content={product.description.substring(0, 160)} />
  <script
    type="application/ld+json"
    dangerouslySetInnerHTML={{ __html: JSON.stringify(generateProductSchema(product)) }}
  />
</Head>

Generate JSON-LD for rich results:

jsCopyEditfunction generateProductSchema(product) {
  return {
    '@context': 'https://schema.org/',
    '@type': 'Product',
    name: product.title,
    images: product.images.edges.map(e => e.node.url),
    offers: {
      '@type': 'Offer',
      priceCurrency: product.variants.edges[0].node.priceV2.currencyCode,
      price: product.variants.edges[0].node.priceV2.amount,
      availability: 'https://schema.org/InStock'
    }
  };
}

7.3 Caching & CDN

  • Deploy on Vercel or Netlify for edge caching of ISR pages.
  • Use Shopify’s CDN for fast asset delivery.

Conclusion

By combining Next.js’s powerful rendering strategies with Shopify’s Hydrogen commerce primitives, you can build a headless storefront that is performant, scalable, and developer-friendly. This approach gives you full control over UI design and user experience, while relying on Shopify’s robust backend for catalog, cart, and checkout. Start by setting up your environment, establish your GraphQL client, scaffold dynamic pages, and leverage Hydrogen hooks to manage cart state. Then, optimize images, SEO tags, and deploy to an edge-optimized platform. Embrace headless commerce today to delight customers and accelerate your development workflow.

Let's connect on TikTok

Join our newsletter to stay updated

Sydney Based Software Solutions Professional who is crafting exceptional systems and applications to solve a diverse range of problems for the past 10 years.

Share the Post

Related Posts