Headless commerce has become increasingly popular over the past several years. It refers to the decoupling of the frontend user interface with the commerce engine.
By decoupling your tech stack, your developers can now build custom frontends with any technology.
In this article, we guide you step-by-step on building this online storefront with Next.js, the popular JavaScript framework.
We will power our storefront with Shopify, the leading ecommerce platform. Shopify’s Storefront API makes going headless with Shopify easy.
We then deploy the Next.js-Shopify storefront to Vercel, the cloud-hosting platform.
If you have any questions during the tutorial, feel free to check out the code repository on GitHub.
This tutorial will walk you through the following steps:
To create a Shopify store, you’ll first need to join the Shopify Partners program.
Create a Shopify Partners account
Select Store on the dashboard
Choose to create a development store
Choose a store name and URL before creating your store.
After creating a development store, you’ll be redirected to an admin panel where you can manage your new store.
First thing you’ll want to do is add some products. For this tutorial, you can import this list of products.
To add products to your store, select Products from the admin panel and import the products. It may take a minute or two for the product to load. But once they do, your products page should look like this:
Now that you’ve created a Shopify store, you’ll need to retrieve a token. This token will allow you to make queries against your product data.
Select Settings from your store dashboard.
Click on Apps and Sales Channels.
Select Develop Apps and Click on Create an App
Name your app and select Create App
Once you’ve created an app, you’ll need to configure the access scope. You can do that by clicking on the Configure Storefront API scopes button.
Give yourself permission to read and modify checkouts and read products, variants, and collections.
Now you’re ready to click on the Install App button on the top right. Once you’ve installed the app, your Storefront API will be available for you to copy.
Let’s go ahead and set-up Next.js locally and configure it for development.
For this tutorial, we’re going to assume you have a basic understanding of JavaScript, React, Next.js, and Tailwind CSS.
In your terminal, add the following code to create a starter Next.js application:
When you run this code, you’ll be asked several questions:
When prompted, type “y” to install the app
Select a name for your project
Select “no” when asked if you want to use TypeScript.
Select “no” when asked if you want to use ESLint
Select “yes” when asked if you want to use Tailwind
Select “no” to use a src/
directory
Select “no” to using the experimental app/
directory
Press return when asked if you want to configure import aliases.
Open up the directory in your favorite code editor. You should see this folder structure:
On the command line in your terminal, run npm run dev
to start your development server. You should be able to visit your new app at localhost:3000
.
Congrats! We’ve created a new Next.js app locally. But we’ll need to make a bunch of changes to this starter app before we start to add our own code.
First, add an image option to your next.js.config
file with the following code:
This allows your app to render images from Shopify.
Next, replace the backgroundImage
extension in the tailwind.js.config
file with this code:
This code adds a custom color to our palette. Feel free to replace #000000
with any value you want.
You’ll also need to remove any CSS from the globals.css
file in the styles/folder
. Your file should only import Tailwind plugins.
Finally, let’s create a .env.local
file to hold our environment variables.
Add your access token and domain name to the .env.local file.
To get your store domain, select Settings in your store admin panel and go to Domains.
Now that we’ve configured our Next.js app properly, let’s add some components. First thing we should do is add a components/
folder in the root directory.
In this folder, we’re going to add a few files. Each file will be a component that we will later import into pages/
.
First, let’s add Header.js
to our new folder.
Feel free to rename the store name and replace the logo.
Now let’s add a Footer.js
, which you can customize however you want.
And now add a Layout.js
file that will wrap our entire app and add our Header
and Footer
component to every page.
Now let’s add ProductCard.js
.
The ProductCard component will receive your product data as a prop
and display it to your shoppers.
In the next section, we’ll import all these components to create our product listing page.
Before we create a product listing page, we first need to make sure that we can request data from the Storefront API.
In the root directory, create a helpers/folder
and add a shopify.js
file. We’ll use this file to retrieve our product data so we can create our listing page.
The callShopify
function requests data from the Storefront API and accepts two arguments: The first is a query and the second are variables.
allProducts
is a query we’ll use to retrieve our products from Shopify.
You’ll also notice that we’ve assigned the String.raw
method to gql
and pass the query as a parameter to it.
You can omit this if you want. This is a hack to get VS Code to highlight the query syntax.
Let’s now delete all the code in your index.js
file. Replace it with the following code.
Before we check out our new product listing page, we’ll need to make sure our header and footer render.
Replace the default code in the pages/_app.js
file with this.
Now let’s rerun npm run dev
and revisit localhost:3000
in your browser. You should see all your products.
You’ll get a 404 error if you click on any of the images on the product listing page. Let’s fix that by creating a detail page for each of our products.
To do that, we’ll first need to update helpers/shopify.js
with two new queries. One query will fetch the slugs for each product page. The other will fetch the details for each product based on the slug.
Create a new file in the pages/
folder and call it [product].js
. In that file, add the code below.
getStaticPaths
tells our Next app how many pages to generate and what their slugs should be. getStaticProps
will then fetch the product details for each of those pages.
We then pass the data as props to ProductDetails
so we can display that data.
We now have a product listing page and a details page for every product. But how does a shopper buy a product?
Let’s start by adding one last query to helpers/shopify.js
.
createCheckout
will request the Storefront API to generate a URL. We can use that URL to redirect the shopper to the Shopify checkout page.
Next, let’s rename hello.js
in the pages
/
api/
folder to checkout.js
. Replace all the code in that file with this.
This will relay a checkout request from the product detail page to the Storefront API and return a URL. We can use this URL to redirect the shopper to the Shopify checkout page.
Finally, let’s make a few updates to the pages/[product].js
so the customer can check out.
We’ve added a bunch of new code to this page.
isLoading
and setIsLoading
to trigger a loading state when the shopper presses the Buy button.
A function which fetches the checkout URL from api/checkout.js
and redirects the user to the Shopify checkout page.
A Buy button which triggers checkout()
and the loading state.
When you visit the detail page and press the Buy button, you’ll be redirected to the Shopify checkout page.
In this section, let’s take the online storefront we built and deploy it.
Before we deploy to Vercel, we’ll want to first push our code to Github.
If you haven’t already, create an account with Github.
Click on Create a New Repository.
Name your repository and select Create Repository.
Then type and return each line of code in your terminal.
Now let’s deploy our code repository to Vercel by following these steps
Create an account with Vercel.
Connect your new account with Github.
Authorize Vercel to have access to your Github repositories.
Import your new storefront repository.
Add the access token and domain name in your .env.local file as environment variables.
Deploy your storefront!
Once you’ve completed all the steps, you should see this:
Congratulations! You’ve built an online storefront with Next.js and the Shopify Storefront API and deployed it to Vercel.
In future articles, we’ll enhance this storefront with new features.
A cart experience
SEO optimization
Collection pages and product filtering
Integration with a headless CMS
npx create-next-app@latest
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
domains: ['images.ctfassets.net'],
},
experimental: {
scrollRestoration: true,
},
}
module.exports = nextConfig
// tailwind.config.file
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
'./app/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {
colors: {
palette: {
primary: "#000000"
}
}
},
},
plugins: [],
}
// styles/global.css
@tailwind base;
@tailwind components;
@tailwind utilities;
SHOPIFY_STORE_FRONT_ACCESS_TOKEN="Place your access token here"
SHOPIFY_STORE_DOMAIN="Place your store domain here"
// components/header.js
import Link from "next/link";
const Header = () => {
return (
<header className="border-b border-palette-lighter sticky top-0 z-20 bg-white">
<Link href="/" passHref>
<div className="flex justify-center mx-auto py-4">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-8 w-8"
viewBox="0 0 20 20"
fill="#000000"
>
<path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" />
</svg>
<span className="text-xl font-bold tracking-tight ml-1">
Comfortably
</span>
</div>
</Link>
</header>
);
}
export default Header;
// components/footer.js
const Footer = () => {
return (
<footer className="py-4 text-center">
<p>A Next.js-Shopify storefront template.</p>
<p>Built by <a href="https://www.commerceworm.com" className="text-palette-primary underline hover:no-underline">Commerceworm</a>.</p>
</footer>
)
}
export default Footer
// components/layout.js
import Header from "./Header"
import Footer from "./Footer"
const Layout = ({ children }) => (
<div>
<Header />
<main>{children}</main>
<Footer />
</div>
)
export default Layout
// components/productcard.js
import Image from "next/image";
import Link from "next/link";
const ProductCard = ({ product }) => {
const id = product.node.id;
const handle = product.node.handle;
const title = product.node.title;
const imageNode = product.node.images.edges[0].node;
const price = product.node.priceRange.maxVariantPrice.amount.replace(
/\.0/g,
""
);
return (
<div
className="
w-full
md:w-1/2
lg:w-1/3
p-2
"
>
<Link href={`/${handle}`} passHref>
<Image
alt=""
src={imageNode.url}
width={imageNode.width}
height={imageNode.height}
className="w-full h-auto"
/>
</Link>
<div>
<p className="text-center text-l font-semibold mx-4 mt-4 mb-1">{title}</p>
<p className="text-center text-gray-700 mb-4">${price}</p>
</div>
</div>
);
}
export default ProductCard;
// helpers/shopify.js
const domain = process.env.SHOPIFY_STORE_DOMAIN
const storefrontAccessToken = process.env.SHOPIFY_STORE_FRONT_ACCESS_TOKEN
export async function callShopify(query, variables = {}) {
const fetchUrl = `https://${domain}/api/2023-04/graphql.json`
const fetchOptions = {
endpoint: fetchUrl,
method: "POST",
headers: {
"X-Shopify-Storefront-Access-Token": storefrontAccessToken,
"Content-Type": "application/json",
},
body: JSON.stringify({ query, variables }),
}
try {
const data = await fetch(fetchUrl, fetchOptions).then((response) =>
response.json()
)
return data
} catch (error) {
console.log(error)
throw new Error("Could not fetch products!")
}
}
const gql = String.raw
export const AllProducts = gql`
query Products {
products(first: 22) {
edges {
node {
id
title
handle
images(first: 10) {
edges {
node {
url
width
height
}
}
}
priceRange {
maxVariantPrice {
amount
}
}
}
}
}
}
`
// pages/index.js
import { Fragment } from "react"
import ProductCard from "../components/ProductCard"
import { callShopify, AllProducts } from "../helpers/shopify"
const Home = ({ products }) => {
return (
<Fragment>
<div className="text-center">
<h1 className="font-bold leading-tight text-palette-primary text-5xl mt-4 py-2 sm:py-4">
Your Home, Reimagined
</h1>
<p className="px-2 text-lg text-gray-600">
Reimagine your living room with our sofas and chairs.
</p>
</div>
<div className="max-w-7xl flex flex-wrap mx-auto px-6 pt-10">
{
products.map((product) => (
<ProductCard key={product.node.id} product={product} />
))
}
</div>
</Fragment>
)
}
export async function getStaticProps() {
const response = await callShopify(AllProducts)
const products = response.data.products.edges
return {
props: {
products
},
}
}
export default Home
// pages/_app.js
import Layout from "../components/Layout"
import "../styles/globals.css"
function MyApp({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
)
}
export default MyApp
// helpers/shopify.js
export const Slugs = gql`
query ProductSlugs {
products(first: 22) {
edges {
node {
handle
}
}
}
}
`
export const singleProduct = gql`
query ProductDetails($handle: String!) {
product(handle: $handle) {
id
title
description
images(first: 10) {
edges {
node {
url
width
height
}
}
}
priceRange {
maxVariantPrice {
amount
}
}
variants(first: 1) {
edges {
node {
id
}
}
}
}
}`
// pages/[product].js
import Image from "next/image"
import { callShopify, Slugs, singleProduct } from "../helpers/shopify"
const ProductDetails = ({ productData }) => {
const imageNode = productData.images.edges[0].node
const title = productData.title
const price = productData.priceRange.maxVariantPrice.amount.replace(/\.0/g, '')
const description = productData.description
const productVariant = productData.variants.edges[0].node.id
return (
<div
className="
px-4
sm:py-12
md:flex-row
py-4 w-full flex flex-col my-0 mx-auto
max-w-7xl
"
>
<div className="w-full flex flex-1">
<div className="w-full h-full relative">
<Image
src={imageNode.url}
alt=""
width={imageNode.width}
height={imageNode.height}
className="w-full h-auto"
/>
</div>
</div>
<div className="pt-2 px-0 md:px-10 pb-8 w-full md:w-1/2">
<h1
className="
sm:mt-0 mt-2 text-5xl font-light leading-large
"
>
{title}
</h1>
<h2 className="text-2xl tracking-wide sm:py-8 py-6">${price}</h2>
<p className="text-gray-600 leading-7">{description}</p>
<div className="my-6"></div>
</div>
</div>
)
}
export async function getStaticPaths() {
const response = await callShopify(Slugs)
const productSlugs = response.data.products.edges
const paths = productSlugs.map((slug) => {
const product = String(slug.node.handle)
return {
params: { product }
}
})
return {
paths,
fallback: false,
}
}
export async function getStaticProps({ params }) {
const response = await callShopify(singleProduct, { handle: params.product })
const productData = response.data.product
return {
props: {
productData,
},
}
}
export default ProductDetails
// helpers/shopify.js
export const createCheckout = gql`
mutation CreateCheckout($variantId: ID!) {
checkoutCreate(input: {
lineItems: [{ variantId: $variantId, quantity: 1 }]
}) {
checkout {
webUrl
}
}
}`
// pages/api/checkout.js
import { callShopify, createCheckout } from "../../helpers/shopify";
export default async function Subscribe(req, res) {
const { variantId } = req.body;
try {
const response = await callShopify(createCheckout, {
variantId,
});
const { webUrl } = response.data.checkoutCreate.checkout;
if (response.status >= 400) {
return res.status(400).json({
error: `There was an error generating the checkoutURL. Please try again.`,
});
}
return res.status(201).json({ checkoutURL: webUrl });
} catch (error) {
console.log(error.message);
return res.status(500).json({
error: `There was an error generating the checkoutURL. Please try again.`,
});
}
}
// updated pages/[product].js
import { useState } from "react"
import Image from "next/image"
import { callShopify, Slugs, singleProduct } from "../helpers/shopify"
const ProductDetails = ({ productData }) => {
const [isLoading, setIsLoading] = useState(false)
const imageNode = productData.images.edges[0].node
const title = productData.title
const price = productData.priceRange.maxVariantPrice.amount.replace(/\.0/g, '')
const description = productData.description
const productVariant = productData.variants.edges[0].node.id
const checkout = async () => {
const fetchUrl = "/api/checkout"
const fetchOptions = {
endpoint: fetchUrl,
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
variantId: productVariant,
}),
}
try {
setIsLoading(true)
const response = await fetch(fetchUrl, fetchOptions)
if (!response.ok) {
let message = await response.json()
message = message.error
throw new Error(message)
}
const data = await response.json()
const { checkoutURL } = data
window.location.href = checkoutURL
} catch (e) {
throw new Error(e)
}
}
return (
<div
className="
px-4
sm:py-12
md:flex-row
py-4 w-full flex flex-col my-0 mx-auto
max-w-7xl
"
>
<div className="w-full flex flex-1">
<div className="w-full h-full relative">
<Image
src={imageNode.url}
alt=""
width={imageNode.width}
height={imageNode.height}
className="w-full h-auto"
/>
</div>
</div>
<div className="pt-2 px-0 md:px-10 pb-8 w-full md:w-1/2">
<h1
className="
sm:mt-0 mt-2 text-5xl font-light leading-large
"
>
{title}
</h1>
<h2 className="text-2xl tracking-wide sm:py-8 py-6">${price}</h2>
<p className="text-gray-600 leading-7">{description}</p>
<div className="my-6"></div>
<button
className="text-sm tracking-wider bg-black text-white font-semibold py-4 px-12 border-2 border-black hover:border-transparent w-full"
onClick={checkout}
>
{isLoading && (
<svg className="inline animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
)}
<span>Buy</span>
</button>
</div>
</div>
)
}
export async function getStaticPaths() {
const response = await callShopify(Slugs)
const productSlugs = response.data.products.edges
const paths = productSlugs.map((slug) => {
const product = String(slug.node.handle)
return {
params: { product }
}
})
return {
paths,
fallback: false,
}
}
export async function getStaticProps({ params }) {
const response = await callShopify(singleProduct, { handle: params.product })
const productData = response.data.product
return {
props: {
productData,
},
}
}
export default ProductDetails
git add -A
git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/<enter your repo address here>.git
git push -u origin main