GraphQL Queries for Implementing a Custom Shopify Storefront

Steps to create a Shopify online shop with Next.js and the Storefront API.

Markus Tripp
5 min readMar 2, 2022

When you decide to create a custom online shop with Shopify, you can program a standard Liquid-based theme or use the Shopify storefront API. Using Shopify as a headless backend and developing the frontend in Next.js or the future, Hydrogen is a good option when performance and custom control are your primary goals. Here I list some useful GraphQL queries to quickly get you up and running.

This article assumes you have some basic knowledge of Next.js, React, and GraphQL. It shows you the required GraphQL queries for creating a Shopify online shop using Shopify’s headless storefront API.

For simplicity, all queries are called from the client. Choosing the correct data fetching strategy for your use case is essential for the best results.

Project setup

I created a standard Next.js project and installed the following additional dependencies:

I structure my directories to reflect the directory structure of Liquid-based themes:

./pages
cart
[[...id]].js

pages
handle1.js
handle2.js
...
policies
legal-notice.js
privacy-policy.js
refund-policy.js
shipping-policy.js
terms-of-service.js
products
[id].js

_app.js
collections.js
index.js

This tutorial shows you the required GraphQL queries for products and cart. I use Server-Side Rendering (SSR — getServerSideProps) to load the initial products and cart data. SSR is also needed if you’d like to optimize your pages with SEO information from the Shopify backend.

Calling the Shopify GraphQL APIs

graphql-request is a minimal GraphQL client library perfect for accessing the Shopify GraphQL API. Therefore I create a small utility class to simplify my calls.

./utils/shopify.jsimport { GraphQLClient } from 'graphql-request'const shopify = async (query, variables) => {
const endpoint = process.env.NEXT_PUBLIC_STOREFRONT_ENDPOINT
const token = process.env.NEXT_PUBLIC_STOREFRONT_API_TOKEN
const graphQLClient = new GraphQLClient(endpoint, {
headers: {
'X-Shopify-Storefront-Access-Token': token,
},
})
return await graphQLClient.request(query, variables)
}
export default shopify

See: Get the API credentials for a custom app and Endpoints and queries.

I can then make the GraphQL calls (e.g., get the product based on the product handle).

import shopify from '../../utils/shopify'...const getProduct = async (id) => {
const query = gql`
query getProduct($handle: String!) {
product(handle: $handle) {
id
title
}
}
`
const variables = { handle: id }
const data = await shopify(query, variables)
}

See: https://shopify.dev/api/storefront/latest/queries/product

Product page

For the product page, I need at least these 2 GraphQL queries:

  • get the product
  • add the product to the shopping cart

I need the cart ID on multiple pages. Therefore I store it in the Window.sessionStorage.

Here is my simplified product page:

./pages/products/[id].jsimport Image from 'next/image'
import { useState } from 'react'
import { gql } from 'graphql-request'
import Layout from '../../components/Layout'
import shopify from '../../utils/shopify'
const getProductQuery = gql`
query getProduct($handle: String!) {
product(handle: $handle) {
id
handle
seo {
title
description
}
title
descriptionHtml
priceRange {
minVariantPrice {
amount
currencyCode
}
}
featuredImage {
url
altText
width
height
}
variants(first: 10) {
edges {
node {
id
}
}
}
}
}
`
const createCartMutation = gql`
mutation cartCreate($input: CartInput) {
cartCreate(input: $input) {
cart {
id
}
}
}
`
const updateCartMutation = gql`
mutation cartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {
cartLinesAdd(cartId: $cartId, lines: $lines) {
cart {
id
}
}
}
`
const Product = ({ product }) => {
const [quantity, setQuantity] = useState(1)
const getLines = () => [
{
quantity: parseInt(quantity),
merchandiseId: product.variants.edges[0].node.id,
},
]
const handleAddToCart = async () => {
let cartId = sessionStorage.getItem('cartId')
if (cartId) {
const variables = {
cartId,
lines: getLines(),
}
const data = await shopify(updateCartMutation, variables)
} else {
const variables = {
input: {
lines: getLines(),
},
}
const data = await shopify(createCartMutation, variables)
cartId = data.cartCreate.cart.id
sessionStorage.setItem('cartId', cartId)
}
}
return (
<Layout>
<h1 className="text-xl">{product.title}</h1>
<div>
<Image
src={product.featuredImage.url}
alt={product.featuredImage.altText}
width={product.featuredImage.width}
height={product.featuredImage.height}
/>
</div>
<div
className="prose"
dangerouslySetInnerHTML={{
__html: product.descriptionHtml,
}}
/>
<div>{product.priceRange.minVariantPrice.amount}</div>
<input
type="number"
name="quantity"
value={quantity}
onChange={(e) => setQuantity(e.target.value)}
/>
<button onClick={handleAddToCart}>
Add to basket
</button>
</Layout>
)
}
export const getServerSideProps = async (context) => {
const { id } = context.query
if (!id) {
return { notFound: true }
}
const variables = { handle: id }
const data = await shopify(getProductQuery, variables)
return {
props: {
product: data.product,
},
}
}
export default Product

Cart page

On the cart page, I have to use at least these 2 GraphQL calls:

  • load all cart lines
  • remove an item from the cart

In this case, I decided to load the cart items on the server using SSR. Therefore the cart link may contain the cart ID in the path. You can achieve this by using Next.js dynamic routes.

./pages/cart/[[…id]].jsimport Link from 'next/link'
import Image from 'next/image'
import { gql } from 'graphql-request'
import shopify from '../../utils/shopify'
import Layout from '../../components/Layout'
const getCartQuery = gql`
query getCart($id: ID!) {
cart(id: $id) {
id
lines(first: 50) {
edges {
node {
id
quantity
merchandise {
... on ProductVariant {
id
title
priceV2 {
amount
currencyCode
}
image {
url
altText
width
height
}
product {
id
title
handle
}
}
}
}
}
}
estimatedCost {
totalAmount {
amount
currencyCode
}
}
checkoutUrl
}
}
`
const removeItemMutation = gql`
mutation cartLinesRemove($cartId: ID!, $lineIds: [ID!]!) {
cartLinesRemove(cartId: $cartId, lineIds: $lineIds) {
cart {
id
}
}
}
`
const Cart = ({ cart }) => {
const handleRemoveItem = async (cartId, lineId) => {
const variables = {
cartId,
lineIds: [lineId],
}
await shopify(removeItemMutation, variables)
}
return (
<Layout>
<h1 className="text-xl">Cart</h1>
<form action={cart.checkoutUrl} method="GET">
<ul>
{cart.lines.edges.map((item) => {
const variant = item.node.merchandise
return (
<li key={item.node.id}>
<div>
<Image
src={variant.image.url}
alt={variant.image.altText}
width={variant.image.width}
height={variant.image.height}
/>
</div>
<Link href={'/products/' + variant.product.handle}>
<a>{variant.product.title}</a>
</Link>
<div>{variant.priceV2.amount}</div>
<div>Quantity: {item.node.quantity}</div>
<button
onClick={() => {
handleRemoveItem(cart.id, item.node.id)
}}
>
Remove
</button>
</li>
)
})}
</ul>
<h2>Total</h2>
<div>{cart.estimatedCost.totalAmount.amount}</div>
<button type="submit">Checkout</button>
</form>
</Layout>
)
}
export const getServerSideProps = async (context) => {
const { id } = context.query
if (!id) return { props: {} }
const variables = { id: id[0] }
const data = await shopify(getCartQuery, variables)
return {
props: {
cart: data.cart,
},
}
}
export default Cart

Tip: Use pagination to retrieve all cart lines! https://shopify.dev/api/usage/pagination-graphql

For a live example, see my latest project (in German).

Conclusion

The future of Shopify is Hydrogen. Hydrogen offers a set of React components to develop custom storefronts. It hides a lot of complexity (e.g., GraphQL queries) from the developer and brings tremendous benefits (e.g., performance improvements). But it’s not available yet.

This tutorial showed you how to use Shopify as a headless e-commerce platform using Next.js/React and native GraphQL queries. It can be seen as a lower level or predecessor of Hydrogen. As soon as Hydrogen is ready, I will most likely migrate my Next.js based Shopify projects.

--

--