Shopify Storefront API
Overview
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.
When to Use This Skill
- When building a headless Shopify storefront with a custom frontend framework
- When creating a React Native or Flutter mobile app that needs product and cart data
- When embedding a Shopify buy button or product widget in a non-Shopify site
- When using Shopify Hydrogen (Remix-based) for a fully custom storefront experience
- When needing real-time product availability or pricing without the Admin API overhead
- When implementing cart persistence across sessions with Shopify's hosted cart
Core Instructions
- 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 theshpat_` 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};}
Examples
Product Detail Page with variant selection (Next.js)
// 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,
}));
}Predictive search
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;
}Best Practices
- Use private tokens server-side — private Storefront Access Tokens have higher rate limits (1000 req/s vs 100 req/s) and should never be exposed to browsers
- Fetch product data at build time when possible (ISR or SSG) — the Storefront API rate limits apply per store, not per customer
- Always check
availableForSaleon both product and variant before showing Add-to-Cart — a product can be available while individual variants are sold out - Paginate with
aftercursors, not offsets — the Storefront API uses cursor-based pagination; storeendCursorfor next-page queries - Cache collection and product queries with Next.js
fetchcache tags or React cache — product data rarely changes in real time - Use
@inContextdirective for international pricing —@inContext(country: CA, language: EN)returns prices in the buyer's currency - Fragment reuse — define GraphQL fragments (e.g.,
ProductFragment) to avoid duplicating field selections across queries - Handle
userErrorson all mutations — cart mutations returnuserErrorsarray; check it before updating local state
Common Pitfalls
| 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 |
Related Skills
- @shopify-admin-api
- @shopify-app-development
- @shopify-checkout-extensions
- @headless-commerce-architecture
- @graphql-api-design