Building Headless Commerce Solutions with Shopify and Next.js
Headless commerce is revolutionizing how we build e-commerce experiences. By decoupling the frontend from the backend, developers gain unprecedented flexibility and performance capabilities.
What is Headless Commerce?
Headless commerce separates the presentation layer (frontend) from the commerce functionality (backend), connecting them via APIs.
Traditional vs. Headless Architecture
Traditional Commerce:
Frontend ← → Backend (Tightly Coupled)
Headless Commerce:
Frontend (Next.js) ← API → Backend (Shopify)
Benefits of Going Headless
- Performance - Optimized frontend with static generation
- Flexibility - Use any frontend framework
- Omnichannel - Same backend for web, mobile, IoT
- Developer Experience - Modern tooling and workflows
- Scalability - Independent scaling of frontend and backend
Setting Up Shopify Storefront API
The Shopify Storefront API is your gateway to headless commerce.
Creating a Private App
- Navigate to Shopify Admin → Apps → App development
- Create a new app
- Configure Storefront API access
- Copy your Storefront Access Token
Environment Configuration
# .env.local
NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=your-store.myshopify.com
NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=your_token_here
SHOPIFY_ADMIN_ACCESS_TOKEN=your_admin_tokenBuilding the GraphQL Client
Shopify's Storefront API uses GraphQL, providing efficient and flexible data fetching.
GraphQL Client Setup
// lib/shopify.ts
import { GraphQLClient } from 'graphql-request';
const endpoint = `https://${process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN}/api/2024-01/graphql.json`;
export const shopifyClient = new GraphQLClient(endpoint, {
headers: {
'X-Shopify-Storefront-Access-Token':
process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN!,
'Content-Type': 'application/json',
},
});
export async function shopifyFetch<T>({
query,
variables,
}: {
query: string;
variables?: Record<string, any>;
}): Promise<T> {
try {
return await shopifyClient.request<T>(query, variables);
} catch (error) {
console.error('Shopify API Error:', error);
throw error;
}
}Fetching Products
// lib/queries/products.ts
import { shopifyFetch } from '../shopify';
const GET_PRODUCTS_QUERY = `
query GetProducts($first: Int!) {
products(first: $first) {
edges {
node {
id
title
handle
description
priceRange {
minVariantPrice {
amount
currencyCode
}
}
images(first: 1) {
edges {
node {
url
altText
}
}
}
}
}
}
}
`;
export async function getProducts(count = 12) {
const data = await shopifyFetch<any>({
query: GET_PRODUCTS_QUERY,
variables: { first: count },
});
return data.products.edges.map(({ node }: any) => ({
id: node.id,
title: node.title,
handle: node.handle,
description: node.description,
price: node.priceRange.minVariantPrice.amount,
currency: node.priceRange.minVariantPrice.currencyCode,
image: node.images.edges[0]?.node.url,
imageAlt: node.images.edges[0]?.node.altText,
}));
}Implementing Product Pages with Next.js
Leverage Next.js's powerful features for optimal performance.
Static Site Generation (SSG)
// app/products/[handle]/page.tsx
import { getProduct, getAllProductHandles } from '@/lib/queries/products';
import { Metadata } from 'next';
export async function generateStaticParams() {
const handles = await getAllProductHandles();
return handles.map((handle) => ({ handle }));
}
export async function generateMetadata({
params
}: {
params: { handle: string }
}): Promise<Metadata> {
const product = await getProduct(params.handle);
return {
title: product.title,
description: product.description,
openGraph: {
images: [product.image],
},
};
}
export default async function ProductPage({
params
}: {
params: { handle: string }
}) {
const product = await getProduct(params.handle);
return (
<div className="container mx-auto px-4 py-12">
<div className="grid md:grid-cols-2 gap-8">
<ProductGallery images={product.images} />
<ProductDetails product={product} />
</div>
</div>
);
}Incremental Static Regeneration (ISR)
// Revalidate every hour
export const revalidate = 3600;
export default async function ProductsPage() {
const products = await getProducts();
return (
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}Shopping Cart Implementation
Building a performant cart experience with React hooks and Shopify's Cart API.
Cart Context
// contexts/CartContext.tsx
'use client';
import { createContext, useContext, useState, useEffect } from 'react';
interface CartItem {
id: string;
title: string;
quantity: number;
price: number;
image: string;
}
interface CartContextType {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
total: number;
}
const CartContext = createContext<CartContextType | undefined>(undefined);
export function CartProvider({ children }: { children: React.ReactNode }) {
const [items, setItems] = useState<CartItem[]>([]);
// Load cart from localStorage
useEffect(() => {
const savedCart = localStorage.getItem('cart');
if (savedCart) {
setItems(JSON.parse(savedCart));
}
}, []);
// Save cart to localStorage
useEffect(() => {
localStorage.setItem('cart', JSON.stringify(items));
}, [items]);
const addItem = (item: CartItem) => {
setItems((prev) => {
const existing = prev.find((i) => i.id === item.id);
if (existing) {
return prev.map((i) =>
i.id === item.id
? { ...i, quantity: i.quantity + item.quantity }
: i
);
}
return [...prev, item];
});
};
const removeItem = (id: string) => {
setItems((prev) => prev.filter((item) => item.id !== id));
};
const updateQuantity = (id: string, quantity: number) => {
setItems((prev) =>
prev.map((item) =>
item.id === id ? { ...item, quantity } : item
)
);
};
const clearCart = () => setItems([]);
const total = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return (
<CartContext.Provider
value={{ items, addItem, removeItem, updateQuantity, clearCart, total }}
>
{children}
</CartContext.Provider>
);
}
export const useCart = () => {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within CartProvider');
}
return context;
};Performance Optimization
Headless commerce enables advanced performance optimizations.
Image Optimization
// components/ProductImage.tsx
import Image from 'next/image';
interface ProductImageProps {
src: string;
alt: string;
priority?: boolean;
}
export function ProductImage({ src, alt, priority = false }: ProductImageProps) {
return (
<div className="relative aspect-square overflow-hidden rounded-lg">
<Image
src={src}
alt={alt}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover"
priority={priority}
/>
</div>
);
}API Route Caching
// app/api/products/route.ts
import { NextResponse } from 'next/server';
import { getProducts } from '@/lib/queries/products';
export async function GET() {
const products = await getProducts();
return NextResponse.json(products, {
headers: {
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
},
});
}SEO Considerations
Headless doesn't mean sacrificing SEO—in fact, it can enhance it.
Structured Data
// components/ProductSchema.tsx
import { Product } from '@/types';
export function ProductSchema({ product }: { product: Product }) {
const schema = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.title,
description: product.description,
image: product.image,
offers: {
'@type': 'Offer',
price: product.price,
priceCurrency: product.currency,
availability: 'https://schema.org/InStock',
},
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}Deployment and Hosting
Deploy your headless commerce site for optimal performance.
Vercel Deployment
# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
# Production deployment
vercel --prodEnvironment Variables
Configure your environment variables in Vercel dashboard:
NEXT_PUBLIC_SHOPIFY_STORE_DOMAINNEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKENSHOPIFY_ADMIN_ACCESS_TOKEN
Conclusion
Headless commerce with Shopify and Next.js offers the best of both worlds: Shopify's powerful commerce backend and Next.js's modern, performant frontend.
This architecture enables you to build lightning-fast, SEO-friendly e-commerce experiences that scale effortlessly.
Ready to go headless? Let's discuss how we can transform your e-commerce platform with modern headless architecture.
