Building a Headless Shopify Store with React and Next.js: A Beginner's Guide

Building a Headless Shopify Store with React and Next.js: A Beginner's Guide
Photo by Andrew Neel on Unsplash

Target Audience:

  • Web Developers
  • Full-stack Developers
  • Shopify Developers
  • E-commerce Developers
  • Beginners in Web Development

Introduction

In the modern e-commerce landscape, creating a custom storefront that offers a seamless user experience is crucial. Headless Shopify allows developers to leverage Shopify's robust backend while using modern frontend technologies like React and Next.js. This guide will walk you through building a foundational skeleton for a headless Shopify store, focusing on:

  • Implementing dynamic pages for products and collections
  • Handling errors using Next.js's built-in error.js
  • Ensuring a beautiful design with great UI/UX
  • Providing proper navigation and file structure
  • Avoiding common errors related to GraphQL queries and image handling

By the end of this tutorial, you'll have a beginner-friendly, fully functional skeleton of a headless Shopify store.


Table of Contents

  1. Prerequisites
  2. Understanding Headless Shopify
  3. Setting Up the Development Environment
  4. Creating a Next.js Project with App Router
  5. Project Structure
  6. Connecting to Shopify's Storefront API
  7. Implementing Dynamic Pages
  8. Handling Errors with error.js
  9. Enhancing UI/UX Design
  10. Final Touches and Testing
  11. Conclusion and Next Steps
  12. Additional Resources

Prerequisites

Before starting, make sure you have:

  • Basic Knowledge of JavaScript and React
  • Node.js Installed: Version 14 or higher
  • npm or Yarn: For package management
  • A Shopify Store: With access to create private apps and obtain API credentials
  • Code Editor: Such as Visual Studio Code
  • Basic Understanding of GraphQL (helpful but not mandatory)

Understanding Headless Shopify

What is Headless Commerce?

Headless commerce refers to the separation of the frontend (presentation layer) from the backend (e-commerce functionality). This allows developers to use any frontend technology to build a custom user experience while leveraging the powerful backend services of platforms like Shopify.

Benefits of Headless Shopify

  • Flexibility: Customize the frontend without limitations
  • Performance: Use modern frameworks optimized for speed
  • Scalability: Easily add new features and integrations
  • Better UI/UX: Design a user interface tailored to your audience

Setting Up the Development Environment

1. Install Node.js and npm

Download and install Node.js from the official website. npm comes bundled with Node.js.

2. Install a Code Editor

We recommend Visual Studio Code for its robust features and extensions.

3. Familiarize Yourself with the Terminal

You'll need to run commands in the terminal or command prompt.


Creating a Next.js Project with App Router

We'll use Next.js with the App Router, introduced in Next.js 13, which provides a more intuitive and flexible routing system.

Step 1: Initialize the Project

Open your terminal and run:

npx create-next-app@latest headless-shopify --use-npm --experimental-app
cd headless-shopify
  • --experimental-app: Enables the App Router feature.

Step 2: Install Dependencies

Install additional packages:

npm install isomorphic-fetch swr graphql
  • isomorphic-fetch: For making API calls on both server and client.
  • swr: A React Hooks library for data fetching.
  • graphql: For constructing GraphQL queries.

Project Structure

Organize your files for clarity and scalability.

headless-shopify/
├── app/
│   ├── error.js
│   ├── layout.js
│   ├── page.js
│   ├── about/
│   │   └── page.js
│   ├── collections/
│   │   ├── [handle]/
│   │   │   └── page.js
│   │   └── page.js
│   ├── products/
│   │   ├── [handle]/
│   │   │   └── page.js
│   │   └── page.js
├── components/
│   ├── Navbar.js
│   ├── Footer.js
│   ├── ProductCard.js
│   ├── CollectionCard.js
├── lib/
│   └── shopify.js
├── public/
│   └── images/
│       └── logo.png
├── styles/
│   └── globals.css
├── .env.local
├── package.json
└── next.config.js

Connecting to Shopify's Storefront API

Step 1: Create a Shopify Private App

  1. Log in to your Shopify admin dashboard.
  2. Navigate to Apps > App and sales channel settings.
  3. Click on Develop apps.
  4. Create a new app and name it (e.g., "Headless Storefront").
  5. Configure the Storefront API with the following permissions:
    • Read Products
    • Read Collections
  6. Install the app.
  7. Copy the Storefront access token.

Step 2: Set Up Environment Variables

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

NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=your-store.myshopify.com
NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=your-storefront-access-token
  • Replace your-store.myshopify.com with your actual store domain.
  • Replace your-storefront-access-token with the token you copied.

Step 3: Create Shopify Utility Functions

In lib/shopify.js:

// lib/shopify.js

const domain = process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN;
const token = process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN;
const storefrontURL = `https://${domain}/api/2023-01/graphql.json`;

export async function ShopifyData(query) {
  const res = await fetch(storefrontURL, {
    method: 'POST',
    headers: {
      'X-Shopify-Storefront-Access-Token': token,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ query }),
  });

  if (!res.ok) {
    const errorDetails = await res.json();
    throw new Error(
      `HTTP error! status: ${res.status}, message: ${errorDetails.errors[0]?.message || 'Unknown error'}`
    );
  }

  const data = await res.json();

  if (data.errors) {
    throw new Error(data.errors[0]?.message || 'Unknown GraphQL error');
  }

  return data;
}

export async function getProducts() {
  const query = `
    {
      products(first: 10) {
        edges {
          node {
            id
            title
            handle
            images(first: 1) {
              edges {
                node {
                  originalSrc
                  altText
                }
              }
            }
            variants(first: 1) {
              edges {
                node {
                  priceV2 {
                    amount
                    currencyCode
                  }
                }
              }
            }
          }
        }
      }
    }
  `;

  const response = await ShopifyData(query);
  return response.data.products.edges.map((edge) => edge.node);
}

export async function getProduct(handle) {
  const query = `
    {
      productByHandle(handle: "${handle}") {
        id
        title
        handle
        description
        images(first: 5) {
          edges {
            node {
              originalSrc
              altText
            }
          }
        }
        variants(first: 1) {
          edges {
            node {
              priceV2 {
                amount
                currencyCode
              }
            }
          }
        }
      }
    }
  `;

  const response = await ShopifyData(query);
  return response.data.productByHandle;
}

export async function getCollections() {
  const query = `
    {
      collections(first: 10) {
        edges {
          node {
            id
            title
            handle
            description
            image {
              originalSrc
              altText
            }
          }
        }
      }
    }
  `;

  const response = await ShopifyData(query);
  return response.data.collections.edges.map((edge) => edge.node);
}

export async function getCollection(handle) {
  const query = `
    {
      collectionByHandle(handle: "${handle}") {
        id
        title
        handle
        description
        image {
          originalSrc
          altText
        }
      }
    }
  `;

  const response = await ShopifyData(query);
  return response.data.collectionByHandle;
}

export async function getCollectionProducts(handle) {
  const query = `
    {
      collectionByHandle(handle: "${handle}") {
        products(first: 10) {
          edges {
            node {
              id
              title
              handle
              images(first: 1) {
                edges {
                  node {
                    originalSrc
                    altText
                  }
                }
              }
              variants(first: 1) {
                edges {
                  node {
                    priceV2 {
                      amount
                      currencyCode
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  `;

  const response = await ShopifyData(query);
  return response.data.collectionByHandle.products.edges.map((edge) => edge.node);
}

Note: We're using priceV2 instead of price to avoid the "Field must have selections" error.


Implementing Dynamic Pages

Understanding Next.js Routing

  • Static Routes: Pages like /about.
  • Dynamic Routes: Pages like /products/[handle] for dynamic content.

1. Home Page (app/page.js)

// app/page.js
import Link from 'next/link';

export default function HomePage() {
  return (
    <main className="container">
      <h1>Welcome to Our Store</h1>
      <p>Your one-stop shop for exclusive products.</p>
      <div className="button-group">
        <Link href="/products">
          <button className="btn-primary">Shop Products</button>
        </Link>
        <Link href="/collections">
          <button className="btn-secondary">Browse Collections</button>
        </Link>
      </div>
    </main>
  );
}

2. Products Page (app/products/page.js)

// app/products/page.js
import { getProducts } from '../../lib/shopify';
import ProductCard from '../../components/ProductCard';

export default async function ProductsPage() {
  const products = await getProducts();

  return (
    <main className="container">
      <h1>All Products</h1>
      <div className="products-grid">
        {products.map((product) => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </main>
  );
}

3. Product Details Page (app/products/[handle]/page.js)

// app/products/[handle]/page.js
import { getProduct } from '../../../lib/shopify';
import Image from 'next/image';

export default async function ProductPage({ params }) {
  const { handle } = params;
  const product = await getProduct(handle);
  const price = product.variants.edges[0].node.priceV2;

  return (
    <main className="container">
      <h1>{product.title}</h1>
      <Image
        src={product.images.edges[0]?.node.originalSrc}
        alt={product.images.edges[0]?.node.altText || product.title}
        width={500}
        height={500}
      />
      <p>{product.description}</p>
      <p>
        Price: {price.currencyCode} {price.amount}
      </p>
      <button className="btn-primary">Add to Cart</button>
    </main>
  );
}

4. Collections Page (app/collections/page.js)

// app/collections/page.js
import { getCollections } from '../../lib/shopify';
import CollectionCard from '../../components/CollectionCard';

export default async function CollectionsPage() {
  const collections = await getCollections();

  return (
    <main className="container">
      <h1>All Collections</h1>
      <div className="collections-grid">
        {collections.map((collection) => (
          <CollectionCard key={collection.id} collection={collection} />
        ))}
      </div>
    </main>
  );
}

5. Collection Details Page (app/collections/[handle]/page.js)

// app/collections/[handle]/page.js
import { getCollection, getCollectionProducts } from '../../../lib/shopify';
import ProductCard from '../../../components/ProductCard';

export default async function CollectionPage({ params }) {
  const { handle } = params;
  const collection = await getCollection(handle);
  const products = await getCollectionProducts(handle);

  return (
    <main className="container">
      <h1>{collection.title}</h1>
      <p>{collection.description}</p>
      <div className="products-grid">
        {products.map((product) => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </main>
  );
}

4. About Us Page

// app/about/page.js
export default function AboutPage() {
  return (
    <main className="container">
      <h1>About Us</h1>
      <p>We are passionate about delivering the best products to our customers.</p>
    </main>
  );
}

Handling Errors with error.js

Next.js 13 provides built-in error handling using the error.js file.

Create the error.js File

Place the error.js file in your app/ directory:

// app/error.js
'use client';

import { useEffect } from 'react';

export default function GlobalError({ error, reset }) {
  useEffect(() => {
    console.error('Global error:', error);
  }, [error]);

  return (
    <html>
      <body>
        <div className="error-container">
          <h2>Oops! Something went wrong.</h2>
          <p>{error.message || 'An unexpected error has occurred.'}</p>
          <button onClick={() => reset()}>Try Again</button>
        </div>
      </body>
    </html>
  );
}

Explanation:

  • 'use client';: Allows the use of client-side hooks.
  • Error Handling: Catches errors thrown during rendering or data fetching.
  • User Feedback: Displays a friendly error message.

Enhancing UI/UX Design

1. Navbar Component

// components/Navbar.js
import Link from 'next/link';
import Image from 'next/image';

export default function Navbar() {
  return (
    <nav className="navbar">
      <Link href="/">
        <Image src="/images/logo.png" alt="Logo" width={100} height={50} />
      </Link>
      <div className="nav-links">
        <Link href="/">Home</Link>
        <Link href="/products">Products</Link>
        <Link href="/collections">Collections</Link>
        <Link href="/about">About Us</Link>
      </div>
    </nav>
  );
}

Note: Ensure that you have logo.png in the public/images directory.

// components/Footer.js
export default function Footer() {
  return (
    <footer className="footer">
      <p>&copy; {new Date().getFullYear()} Our Store. All rights reserved.</p>
    </footer>
  );
}

3. Layout Component

Update app/layout.js:

// app/layout.js
import Navbar from '../components/Navbar';
import Footer from '../components/Footer';
import '../styles/globals.css';

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <Navbar />
        {children}
        <Footer />
      </body>
    </html>
  );
}

4. Styling with CSS

Create styles/globals.css:

/* styles/globals.css */

body {
  margin: 0;
  font-family: Arial, sans-serif;
  background-color: #f5f5f5;
}

.container {
  padding: 20px;
}

.navbar {
  display: flex;
  align-items: center;
  background-color: #333;
  padding: 10px 20px;
}

.navbar a {
  color: #fff;
  margin-left: 20px;
  text-decoration: none;
}

.footer {
  text-align: center;
  padding: 10px;
  background-color: #333;
  color: #fff;
}

.btn-primary,
.btn-secondary {
  padding: 10px 20px;
  border: none;
  cursor: pointer;
  margin-right: 10px;
}

.btn-primary {
  background-color: #0070f3;
  color: #fff;
}

.btn-secondary {
  background-color: #555;
  color: #fff;
}

.products-grid,
.collections-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
}

.product-card,
.collection-card {
  background-color: #fff;
  padding: 10px;
  width: calc(33.333% - 20px);
  box-sizing: border-box;
}

.button-group {
  display: flex;
  gap: 10px;
}

.error-container {
  text-align: center;
  padding: 50px;
}

5. ProductCard Component

// components/ProductCard.js
import Link from 'next/link';
import Image from 'next/image';

export default function ProductCard({ product }) {
  const price = product.variants.edges[0].node.priceV2;
  return (
    <div className="product-card">
      <Link href={`/products/${product.handle}`}>
        <Image
          src={product.images.edges[0]?.node.originalSrc}
          alt={product.images.edges[0]?.node.altText || product.title}
          width={200}
          height={200}
        />
      </Link>
      <h3>{product.title}</h3>
      <p>
        Price: {price.currencyCode} {price.amount}
      </p>
    </div>
  );
}

6. CollectionCard Component

// components/CollectionCard.js
import Link from 'next/link';
import Image from 'next/image';

export default function CollectionCard({ collection }) {
  return (
    <div className="collection-card">
      <Link href={`/collections/${collection.handle}`}>
        <Image
          src={collection.image?.originalSrc}
          alt={collection.image?.altText || collection.title}
          width={200}
          height={200}
        />
      </Link>
      <h3>{collection.title}</h3>
    </div>
  );
}

Final Touches and Testing

Update next.config.js

To avoid the "Invalid src prop on next/image" error, configure external image domains.

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
    reactStrictMode: true,
    images: {
      domains: ['cdn.shopify.com'],
    },
  };

export default nextConfig;

Restart the Development Server

After making changes to next.config.js, restart your server:

npm run dev

Test the Application

  • Home Page: Visit http://localhost:3000/ to see the home page.
  • Products Page: Click on "Shop Products" to view products.
  • Product Details: Click on a product to see its details.
  • Collections Page: Click on "Browse Collections" to view collections.
  • Collection Details: Click on a collection to see its products.

Conclusion and Next Steps

Congratulations! You've successfully built a foundational skeleton for a headless Shopify store using React and Next.js. You've learned how to:

  • Set up a Next.js project with the App Router
  • Connect to Shopify's Storefront API
  • Implement dynamic pages for products and collections
  • Handle errors using Next.js's built-in error.js
  • Enhance the UI/UX with custom components and styling
  • Avoid common errors related to GraphQL queries and image handling

Next Steps:

  • Implement Shopping Cart Functionality: Allow users to add products to a cart.
  • Add User Authentication: Enable users to create accounts and sign in.
  • Optimize for SEO: Improve search engine visibility.
  • Enhance Performance: Implement code splitting and optimize images.
  • Add More Features: Include product reviews, search functionality, etc.

Additional Resources


Feel free to leave comments or questions below. Happy coding!