GraphQL Queries for Implementing a Custom Shopify Storefront

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

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

  • get the product
  • add the product to the shopping cart
./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

  • load all cart lines
  • remove an item from the cart
./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

Conclusion

--

--

I ❤️ Shopify & Next.js. checkout-agency.com, Founder

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