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 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 };
}
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
availableForSale on both product and variant before showing Add-to-Cart — a product can be available while individual variants are sold out
- Paginate with
after cursors, not offsets — the Storefront API uses cursor-based pagination; store endCursor for next-page queries
- Cache collection and product queries with Next.js
fetch cache tags or React cache — product data rarely changes in real time
- Use
@inContext directive 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
userErrors on all mutations — cart mutations return userErrors array; 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
1---2name: shopify-storefront-api3description: Build a headless Shopify frontend using the GraphQL Storefront API for product queries, cart management, and checkout with the Buy SDK4---56# Shopify Storefront API78## Overview910The 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.1112## When to Use This Skill1314- When building a headless Shopify storefront with a custom frontend framework15- When creating a React Native or Flutter mobile app that needs product and cart data16- When embedding a Shopify buy button or product widget in a non-Shopify site17- When using Shopify Hydrogen (Remix-based) for a fully custom storefront experience18- When needing real-time product availability or pricing without the Admin API overhead19- When implementing cart persistence across sessions with Shopify's hosted cart2021## Core Instructions22231. **Create a Storefront Access Token**2425 In Shopify Admin → Apps → Develop apps → Your App → API credentials → Storefront API access token. Or via the Admin API:2627 ```javascript28 // Via Admin API (one-time setup)29 const token = await admin.graphql(`30 mutation {31 storefrontAccessTokenCreate(input: { title: "Headless Frontend" }) {32 storefrontAccessToken {33 accessToken34 title35 }36 userErrors { field message }37 }38 }39 `);40 ```4142 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.43442. **Set up the Storefront API client**4546 Using the official `@shopify/storefront-api-client`:4748 ```bash49 npm install @shopify/storefront-api-client50 ```5152 ```typescript53 // lib/shopify.ts54 import { createStorefrontApiClient } from "@shopify/storefront-api-client";5556 export const storefront = createStorefrontApiClient({57 storeDomain: process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN!, // e.g. "mystore.myshopify.com"58 apiVersion: "2025-01",59 publicAccessToken: process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_TOKEN!,60 });61 ```6263 For server-side calls with a private access token (higher rate limits):6465 ```typescript66 export const storefrontServer = createStorefrontApiClient({67 storeDomain: process.env.SHOPIFY_STORE_DOMAIN!,68 apiVersion: "2025-01",69 privateAccessToken: process.env.SHOPIFY_STOREFRONT_PRIVATE_TOKEN!,70 });71 ```72733. **Query products and collections**7475 ```typescript76 // lib/products.ts77 export async function getProducts(first = 20, after?: string) {78 const { data, errors } = await storefront.request(`79 query GetProducts($first: Int!, $after: String) {80 products(first: $first, after: $after, sortKey: BEST_SELLING) {81 pageInfo {82 hasNextPage83 endCursor84 }85 edges {86 node {87 id88 title89 handle90 availableForSale91 priceRange {92 minVariantPrice { amount currencyCode }93 maxVariantPrice { amount currencyCode }94 }95 images(first: 1) {96 edges {97 node { url altText width height }98 }99 }100 variants(first: 10) {101 edges {102 node {103 id104 title105 availableForSale106 selectedOptions { name value }107 price { amount currencyCode }108 }109 }110 }111 }112 }113 }114 }115 `, { variables: { first, after } });116117 if (errors) throw new Error(errors.message);118 return data.products;119 }120 ```1211224. **Create and manage a cart**123124 ```typescript125 // lib/cart.ts126127 // Create a new cart128 export async function cartCreate(lines: { merchandiseId: string; quantity: number }[]) {129 const { data } = await storefront.request(`130 mutation CartCreate($lines: [CartLineInput!]) {131 cartCreate(input: { lines: $lines }) {132 cart {133 id134 checkoutUrl135 lines(first: 50) {136 edges {137 node {138 id139 quantity140 merchandise {141 ... on ProductVariant {142 id143 title144 price { amount currencyCode }145 product { title handle }146 }147 }148 }149 }150 }151 cost {152 subtotalAmount { amount currencyCode }153 totalAmount { amount currencyCode }154 }155 }156 userErrors { field message }157 }158 }159 `, { variables: { lines } });160 return data.cartCreate;161 }162163 // Add lines to existing cart164 export async function cartLinesAdd(cartId: string, lines: { merchandiseId: string; quantity: number }[]) {165 const { data } = await storefront.request(`166 mutation CartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {167 cartLinesAdd(cartId: $cartId, lines: $lines) {168 cart { id checkoutUrl }169 userErrors { field message }170 }171 }172 `, { variables: { cartId, lines } });173 return data.cartLinesAdd;174 }175 ```1761775. **Persist cart ID and redirect to checkout**178179 ```typescript180 // hooks/useCart.ts181 import { useState, useEffect } from "react";182 import { cartCreate, cartLinesAdd } from "../lib/cart";183184 const CART_ID_KEY = "shopify_cart_id";185186 export function useCart() {187 const [cartId, setCartId] = useState<string | null>(null);188 const [checkoutUrl, setCheckoutUrl] = useState<string | null>(null);189190 useEffect(() => {191 setCartId(localStorage.getItem(CART_ID_KEY));192 }, []);193194 const addToCart = async (variantId: string, quantity = 1) => {195 const lines = [{ merchandiseId: variantId, quantity }];196 if (cartId) {197 const result = await cartLinesAdd(cartId, lines);198 setCheckoutUrl(result.cart.checkoutUrl);199 } else {200 const result = await cartCreate(lines);201 const newCartId = result.cart.id;202 localStorage.setItem(CART_ID_KEY, newCartId);203 setCartId(newCartId);204 setCheckoutUrl(result.cart.checkoutUrl);205 }206 };207208 const goToCheckout = () => {209 if (checkoutUrl) window.location.href = checkoutUrl;210 };211212 return { addToCart, goToCheckout, cartId };213 }214 ```215216## Examples217218### Product Detail Page with variant selection (Next.js)219220```typescript221// app/products/[handle]/page.tsx222import { storefront } from "@/lib/shopify";223224async function getProduct(handle: string) {225 const { data } = await storefront.request(`226 query GetProduct($handle: String!) {227 product(handle: $handle) {228 id229 title230 descriptionHtml231 seo { title description }232 images(first: 10) {233 edges { node { url altText } }234 }235 options {236 id name values237 }238 variants(first: 100) {239 edges {240 node {241 id242 availableForSale243 selectedOptions { name value }244 price { amount currencyCode }245 compareAtPrice { amount currencyCode }246 }247 }248 }249 }250 }251 `, { variables: { handle } });252 return data.product;253}254255export default async function ProductPage({ params }: { params: { handle: string } }) {256 const product = await getProduct(params.handle);257 // Render product with client-side variant picker258 return <ProductDetail product={product} />;259}260261// Generate static params for all products262export async function generateStaticParams() {263 const { data } = await storefront.request(`264 query { products(first: 200) { edges { node { handle } } } }265 `);266 return data.products.edges.map(({ node }: { node: { handle: string } }) => ({267 handle: node.handle,268 }));269}270```271272### Predictive search273274```typescript275export async function predictiveSearch(query: string) {276 const { data } = await storefront.request(`277 query PredictiveSearch($query: String!) {278 predictiveSearch(query: $query, limit: 5, types: [PRODUCT, COLLECTION, ARTICLE]) {279 products {280 id title handle281 featuredImage { url altText }282 priceRange { minVariantPrice { amount currencyCode } }283 }284 collections {285 id title handle286 image { url altText }287 }288 }289 }290 `, { variables: { query } });291 return data.predictiveSearch;292}293```294295## Best Practices296297- **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 browsers298- **Fetch product data at build time** when possible (ISR or SSG) — the Storefront API rate limits apply per store, not per customer299- **Always check `availableForSale`** on both product and variant before showing Add-to-Cart — a product can be available while individual variants are sold out300- **Paginate with `after` cursors**, not offsets — the Storefront API uses cursor-based pagination; store `endCursor` for next-page queries301- **Cache collection and product queries** with Next.js `fetch` cache tags or React cache — product data rarely changes in real time302- **Use `@inContext` directive** for international pricing — `@inContext(country: CA, language: EN)` returns prices in the buyer's currency303- **Fragment reuse** — define GraphQL fragments (e.g., `ProductFragment`) to avoid duplicating field selections across queries304- **Handle `userErrors`** on all mutations — cart mutations return `userErrors` array; check it before updating local state305306## Common Pitfalls307308| Problem | Solution |309|---------|----------|310| Rate limit errors (429) | Use private access token server-side and implement request batching; avoid N+1 product queries |311| Cart ID lost after page reload | Persist `cartId` in `localStorage` or a cookie; create a new cart only if none exists |312| Product prices show in wrong currency | Add `@inContext(country: $country)` directive and pass buyer's country via geolocation |313| `product(handle:)` returns null | Handle slugified handles correctly — Shopify handles are lowercase with hyphens; check exact slug |314| Checkout redirect fails on mobile Safari | Use `window.location.href = checkoutUrl` inside a user gesture handler, not async callback |315| Variant not found when selecting options | Use client-side filtering of `variants.edges` by matching all `selectedOptions`, not just one |316317## Related Skills318319- @shopify-admin-api320- @shopify-app-development321- @shopify-checkout-extensions322- @headless-commerce-architecture323- @graphql-api-design