shopify-storefront-api
Build a headless Shopify frontend using the GraphQL Storefront API for product queries, cart management, and checkout with the Buy SDK
How do I install this agent skill?
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill shopify-storefront-apiIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The skill provides standard instructions and code examples for integrating Shopify's Storefront API into headless applications. It follows security best practices for handling access tokens and uses official Shopify libraries.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
What does this agent skill do?
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
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
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/finsilabs/awesome-ecommerce-skills/shopify-storefront-api">View shopify-storefront-api on skillZs</a>