GraphQL Queries for Implementing a Custom Shopify Storefront

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

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

./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

Calling the Shopify GraphQL APIs

./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.

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)
}

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

./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

Conclusion

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store