Building a Headless Shopify Store with React and Next.js: A Beginner's Guide
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
- Prerequisites
- Understanding Headless Shopify
- Setting Up the Development Environment
- Creating a Next.js Project with App Router
- Project Structure
- Connecting to Shopify's Storefront API
- Implementing Dynamic Pages
- Handling Errors with
error.js
- Enhancing UI/UX Design
- Final Touches and Testing
- Conclusion and Next Steps
- 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
- Log in to your Shopify admin dashboard.
- Navigate to Apps > App and sales channel settings.
- Click on Develop apps.
- Create a new app and name it (e.g., "Headless Storefront").
- Configure the Storefront API with the following permissions:
- Read Products
- Read Collections
- Install the app.
- 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.
2. Footer Component
// components/Footer.js
export default function Footer() {
return (
<footer className="footer">
<p>© {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
- Next.js Documentation: https://nextjs.org/docs
- Shopify Storefront API: https://shopify.dev/api/storefront
- React Documentation: https://reactjs.org/docs/getting-started.html
- GraphQL Introduction: https://graphql.org/learn/
- SWR Documentation: https://swr.vercel.app/
- Shopify GraphiQL App: https://shopify.dev/tools/graphiql-app
Feel free to leave comments or questions below. Happy coding!