About shopify-storefront-api
A development skill for building custom Shopify storefronts with the Storefront GraphQL API, aimed at frontend developers. It explains token setup, product and collection queries, cart operations, and patterns for headless sites and mobile apps.
Key features
- 01Create a Storefront Access Token
- 02Query products and collections with GraphQL
- 03Create carts and manage line items
- 04Persist cart IDs and redirect to checkout
- 05Implement predictive search and variant selection
Use cases
- 01Build a headless storefront with Next.js or Remix
- 02Read product and cart data in a mobile app
- 03Embed a buy widget on a non-Shopify site
- 04Persist carts across sessions
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ finsilabs/ awesome-ecommerce-skills --skill shopify-storefront-apiAfter installing, type/shopify-storefront-apiin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
tile.jsonevals/cart-persistence-usererrors-handling-var/criteria.jsontask.mdevals/cursor-pagination-incontext-directive-fr/criteria.jsontask.mdevals/storefront-client-setup-with-public-and-/criteria.jsontask.mdDetails
From the project READMEShopify 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
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