skills/platform-shopify/shopify-storefront-api/SKILL.md
Build a headless Shopify frontend using the GraphQL Storefront API for product queries, cart management, and checkout with the Buy SDK
npx skillsauth add finsilabs/awesome-ecommerce-skills shopify-storefront-apiInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The Shopify Storefront API is a public-facing GraphQL API that provides read and write access to a store's products, collections, cart, and checkout from any frontend. It uses a Storefront Access Token (distinct from Admin API tokens) and is safe to expose in client-side JavaScript. Use it to build headless storefronts with Next.js, Remix/Hydrogen, or any JS framework.
Create a Storefront Access Token
In Shopify Admin → Apps → Develop apps → Your App → API credentials → Storefront API access token. Or via the Admin API:
// Via Admin API (one-time setup)
const token = await admin.graphql(`
mutation {
storefrontAccessTokenCreate(input: { title: "Headless Frontend" }) {
storefrontAccessToken {
accessToken
title
}
userErrors { field message }
}
}
`);
Storefront Access Tokens do not use the shpat_ prefix (that prefix is for Admin API tokens). Storefront tokens are opaque strings safe to use in browser code — they only allow storefront-scoped operations.
Set up the Storefront API client
Using the official @shopify/storefront-api-client:
npm install @shopify/storefront-api-client
// lib/shopify.ts
import { createStorefrontApiClient } from "@shopify/storefront-api-client";
export const storefront = createStorefrontApiClient({
storeDomain: process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN!, // e.g. "mystore.myshopify.com"
apiVersion: "2025-01",
publicAccessToken: process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_TOKEN!,
});
For server-side calls with a private access token (higher rate limits):
export const storefrontServer = createStorefrontApiClient({
storeDomain: process.env.SHOPIFY_STORE_DOMAIN!,
apiVersion: "2025-01",
privateAccessToken: process.env.SHOPIFY_STOREFRONT_PRIVATE_TOKEN!,
});
Query products and collections
// lib/products.ts
export async function getProducts(first = 20, after?: string) {
const { data, errors } = await storefront.request(`
query GetProducts($first: Int!, $after: String) {
products(first: $first, after: $after, sortKey: BEST_SELLING) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
title
handle
availableForSale
priceRange {
minVariantPrice { amount currencyCode }
maxVariantPrice { amount currencyCode }
}
images(first: 1) {
edges {
node { url altText width height }
}
}
variants(first: 10) {
edges {
node {
id
title
availableForSale
selectedOptions { name value }
price { amount currencyCode }
}
}
}
}
}
}
}
`, { variables: { first, after } });
if (errors) throw new Error(errors.message);
return data.products;
}
Create and manage a cart
// lib/cart.ts
// Create a new cart
export async function cartCreate(lines: { merchandiseId: string; quantity: number }[]) {
const { data } = await storefront.request(`
mutation CartCreate($lines: [CartLineInput!]) {
cartCreate(input: { lines: $lines }) {
cart {
id
checkoutUrl
lines(first: 50) {
edges {
node {
id
quantity
merchandise {
... on ProductVariant {
id
title
price { amount currencyCode }
product { title handle }
}
}
}
}
}
cost {
subtotalAmount { amount currencyCode }
totalAmount { amount currencyCode }
}
}
userErrors { field message }
}
}
`, { variables: { lines } });
return data.cartCreate;
}
// Add lines to existing cart
export async function cartLinesAdd(cartId: string, lines: { merchandiseId: string; quantity: number }[]) {
const { data } = await storefront.request(`
mutation CartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {
cartLinesAdd(cartId: $cartId, lines: $lines) {
cart { id checkoutUrl }
userErrors { field message }
}
}
`, { variables: { cartId, lines } });
return data.cartLinesAdd;
}
Persist cart ID and redirect to checkout
// hooks/useCart.ts
import { useState, useEffect } from "react";
import { cartCreate, cartLinesAdd } from "../lib/cart";
const CART_ID_KEY = "shopify_cart_id";
export function useCart() {
const [cartId, setCartId] = useState<string | null>(null);
const [checkoutUrl, setCheckoutUrl] = useState<string | null>(null);
useEffect(() => {
setCartId(localStorage.getItem(CART_ID_KEY));
}, []);
const addToCart = async (variantId: string, quantity = 1) => {
const lines = [{ merchandiseId: variantId, quantity }];
if (cartId) {
const result = await cartLinesAdd(cartId, lines);
setCheckoutUrl(result.cart.checkoutUrl);
} else {
const result = await cartCreate(lines);
const newCartId = result.cart.id;
localStorage.setItem(CART_ID_KEY, newCartId);
setCartId(newCartId);
setCheckoutUrl(result.cart.checkoutUrl);
}
};
const goToCheckout = () => {
if (checkoutUrl) window.location.href = checkoutUrl;
};
return { addToCart, goToCheckout, cartId };
}
// app/products/[handle]/page.tsx
import { storefront } from "@/lib/shopify";
async function getProduct(handle: string) {
const { data } = await storefront.request(`
query GetProduct($handle: String!) {
product(handle: $handle) {
id
title
descriptionHtml
seo { title description }
images(first: 10) {
edges { node { url altText } }
}
options {
id name values
}
variants(first: 100) {
edges {
node {
id
availableForSale
selectedOptions { name value }
price { amount currencyCode }
compareAtPrice { amount currencyCode }
}
}
}
}
}
`, { variables: { handle } });
return data.product;
}
export default async function ProductPage({ params }: { params: { handle: string } }) {
const product = await getProduct(params.handle);
// Render product with client-side variant picker
return <ProductDetail product={product} />;
}
// Generate static params for all products
export async function generateStaticParams() {
const { data } = await storefront.request(`
query { products(first: 200) { edges { node { handle } } } }
`);
return data.products.edges.map(({ node }: { node: { handle: string } }) => ({
handle: node.handle,
}));
}
export async function predictiveSearch(query: string) {
const { data } = await storefront.request(`
query PredictiveSearch($query: String!) {
predictiveSearch(query: $query, limit: 5, types: [PRODUCT, COLLECTION, ARTICLE]) {
products {
id title handle
featuredImage { url altText }
priceRange { minVariantPrice { amount currencyCode } }
}
collections {
id title handle
image { url altText }
}
}
}
`, { variables: { query } });
return data.predictiveSearch;
}
availableForSale on both product and variant before showing Add-to-Cart — a product can be available while individual variants are sold outafter cursors, not offsets — the Storefront API uses cursor-based pagination; store endCursor for next-page queriesfetch cache tags or React cache — product data rarely changes in real time@inContext directive for international pricing — @inContext(country: CA, language: EN) returns prices in the buyer's currencyProductFragment) to avoid duplicating field selections across queriesuserErrors on all mutations — cart mutations return userErrors array; check it before updating local state| Problem | Solution |
|---------|----------|
| Rate limit errors (429) | Use private access token server-side and implement request batching; avoid N+1 product queries |
| Cart ID lost after page reload | Persist cartId in localStorage or a cookie; create a new cart only if none exists |
| Product prices show in wrong currency | Add @inContext(country: $country) directive and pass buyer's country via geolocation |
| product(handle:) returns null | Handle slugified handles correctly — Shopify handles are lowercase with hyphens; check exact slug |
| Checkout redirect fails on mobile Safari | Use window.location.href = checkoutUrl inside a user gesture handler, not async callback |
| Variant not found when selecting options | Use client-side filtering of variants.edges by matching all selectedOptions, not just one |
tools
Let shoppers save products to a wishlist, share it with friends, and get notified when saved items come back in stock or drop in price
development
Build a themeable storefront with design tokens and CSS custom properties that supports white-labeling, multi-brand variants, and dark mode
development
Speed up product discovery with instant search suggestions, fuzzy typo matching, and category-aware results powered by Algolia or Elasticsearch
development
Build a mobile-first storefront with thumb-friendly navigation, sticky add-to-cart buttons, and touch-optimized components for high mobile conversion