
Shopify Storefront Api
- 75 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Build a headless Shopify frontend using the GraphQL Storefront API for product queries, cart management, and checkout with the Buy SDK.
About
Uses the Shopify GraphQL Storefront API and Buy SDK for product queries, cart management, and checkout in a headless frontend. A developer uses it to build custom storefronts against Shopify.
- GraphQL Storefront API for products and cart
- Checkout via the Buy SDK
Shopify Storefront Api by the numbers
- 75 all-time installs (skills.sh)
- Ranked #3,062 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill shopify-storefront-apiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 75 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Build a headless Shopify frontend using the GraphQL Storefront API for product queries, cart management, and checkout with the Buy SDK.
Files
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
1. 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.
2. 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!,
});3. 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;
}4. 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;
}5. 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
endCursorfor next-page queries - Cache collection and product queries with Next.js
fetchcache 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
userErrorsarray; 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
{
"context": "Tests whether the agent correctly persists the cart ID in localStorage, checks userErrors after cart mutations, implements variant option matching across ALL selected options, and handles the checkout redirect correctly for cross-browser compatibility.",
"type": "weighted_checklist",
"checklist": [
{
"name": "cartId stored in localStorage",
"max_score": 10,
"description": "The cart ID is stored in localStorage (e.g. localStorage.setItem(...)) after cart creation"
},
{
"name": "cartId read from localStorage",
"max_score": 10,
"description": "On initialisation the cart ID is retrieved from localStorage (e.g. localStorage.getItem(...)) before attempting to create a new cart"
},
{
"name": "New cart only if none exists",
"max_score": 10,
"description": "cartCreate is called ONLY when there is no existing cart ID; existing carts reuse cartLinesAdd instead"
},
{
"name": "userErrors checked after cartCreate",
"max_score": 8,
"description": "The userErrors field is included in the cartCreate mutation response AND its value is checked/handled before updating state"
},
{
"name": "userErrors checked after cartLinesAdd",
"max_score": 8,
"description": "The userErrors field is included in the cartLinesAdd mutation response AND its value is checked/handled before updating state"
},
{
"name": "All selectedOptions matched for variant",
"max_score": 12,
"description": "Variant lookup compares ALL selectedOptions (e.g. both size and color) when finding the matching variant, not just one option"
},
{
"name": "Does NOT match on single option",
"max_score": 8,
"description": "Variant selection does NOT short-circuit on a single matching option — every selectedOption must match"
},
{
"name": "window.location.href for checkout redirect",
"max_score": 10,
"description": "Checkout redirect uses window.location.href = checkoutUrl (not router.push, window.open, or an <a> tag navigation)"
},
{
"name": "Checkout redirect in user gesture handler",
"max_score": 12,
"description": "The window.location.href assignment is inside a synchronous click/event handler, NOT inside an async callback or Promise.then()"
},
{
"name": "checkoutUrl returned from cart mutations",
"max_score": 12,
"description": "The cartCreate and/or cartLinesAdd mutation response includes the checkoutUrl field"
}
]
}
Shopping Cart for Headless Shopify Storefront
Problem/Feature Description
Maple Street Outfitters is a Canadian apparel brand running their headless Shopify storefront built with Next.js and React. Their product catalog features items with multiple option axes — for example, a jacket that comes in three sizes (S, M, L) and two colours (Navy, Forest Green), giving six distinct variants. Customers have complained that the cart sometimes loses their items after navigating to another page and coming back, and that the checkout redirect occasionally breaks on iPhone Safari. The team also wants the cart to fail gracefully rather than silently swallowing errors.
You have been brought in to implement a robust cart module. The Shopify client is already available and products with variants are already fetched — your task is to build the cart logic and a React hook that components can call.
Output Specification
Produce the following files:
lib/cart.ts— Exports the following async functions (using the Shopify Storefront API GraphQL mutations):cartCreate(lines: { merchandiseId: string; quantity: number }[])— creates a new cartcartLinesAdd(cartId: string, lines: { merchandiseId: string; quantity: number }[])— adds lines to an existing cart
hooks/useCart.ts— A React hook that:- Persists and restores the cart across page reloads
- Exposes
addToCart(variantId: string, quantity?: number)that correctly reuses an existing cart if one is saved, or creates a new one - Exposes
goToCheckout()that redirects to the Shopify checkout URL
lib/variants.ts— Exports a functionfindVariant(variants: ProductVariant[], selectedOptions: Record<string, string>)that returns the matching variant given the user's currently selected option values (e.g.{ Size: "M", Color: "Navy" })
You can assume lib/shopify.ts already exports a storefront client. Do not make live API calls — the implementation only needs to be structurally correct and follow good practices for the Shopify platform.
Input Files
The following types are provided. Extract them before beginning.
=============== FILE: types/shopify.ts =============== export interface SelectedOption { name: string; value: string; }
export interface ProductVariant { id: string; title: string; availableForSale: boolean; selectedOptions: SelectedOption[]; price: { amount: string; currencyCode: string }; }
{
"context": "Tests whether the agent correctly implements cursor-based pagination (not offset-based), uses the @inContext directive for international pricing, reuses GraphQL fragments across queries, checks availableForSale on both product and variant, and applies BEST_SELLING sort key.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Cursor-based pagination",
"max_score": 10,
"description": "Pagination uses an 'after' cursor argument (not skip/offset), e.g. products(first: N, after: $cursor)"
},
{
"name": "pageInfo fields",
"max_score": 10,
"description": "Product list query includes pageInfo { hasNextPage endCursor } to support next-page fetching"
},
{
"name": "endCursor stored/returned",
"max_score": 8,
"description": "The endCursor value is stored or returned from the fetch function so the caller can request the next page"
},
{
"name": "@inContext directive present",
"max_score": 12,
"description": "At least one GraphQL query uses the @inContext directive (e.g. @inContext(country: $country)) to localise pricing"
},
{
"name": "country passed to @inContext",
"max_score": 8,
"description": "The @inContext directive receives a country variable (or hardcoded country code), not just a language"
},
{
"name": "GraphQL fragment defined",
"max_score": 10,
"description": "A named GraphQL fragment (e.g. fragment ProductFields on Product) is defined and reused in at least one query"
},
{
"name": "availableForSale on product",
"max_score": 8,
"description": "The product query selects availableForSale at the product level"
},
{
"name": "availableForSale on variant",
"max_score": 8,
"description": "The product query selects availableForSale at the variant level"
},
{
"name": "BEST_SELLING sort key",
"max_score": 8,
"description": "Product list query uses sortKey: BEST_SELLING"
},
{
"name": "Does NOT use offset pagination",
"max_score": 8,
"description": "No use of skip, offset, or page number parameters for pagination"
},
{
"name": "priceRange or price in buyer currency",
"max_score": 10,
"description": "Price fields (priceRange or variant price) are included in the query alongside the @inContext directive to return localised prices"
}
]
}
International Product Catalog for Headless Shopify
Problem/Feature Description
Volta Goods is a European outdoor equipment brand using Shopify as their backend. They are launching a headless Next.js storefront that will serve customers across multiple countries. Their product catalog has hundreds of items, so the product listing page must load products in batches rather than all at once. Additionally, customers visiting from France should see prices in EUR, customers from the UK in GBP, and so on — the existing Shopify-hosted store already manages multi-currency pricing, so the data layer just needs to surface it correctly.
The engineering team has set up the Shopify client already (it's available as storefront exported from lib/shopify.ts). Your job is to implement the product data-fetching layer so the catalog page can display localised product prices and load more products on demand. The team uses TypeScript throughout.
Output Specification
Produce the following TypeScript file:
lib/products.ts— Exports the following functions:getProducts(first: number, after?: string, country?: string): Promise<...>— fetches a page of products; thecountryparameter (ISO 3166-1 alpha-2 code, e.g."FR","GB") should influence the currency of prices returnedgetProduct(handle: string, country?: string): Promise<...>— fetches a single product by its URL handle
Both functions should return enough data to render a product card or product detail page (title, handle, prices, images, availability).
You can assume lib/shopify.ts already exports:
import { createStorefrontApiClient } from "@shopify/storefront-api-client";
export const storefront = createStorefrontApiClient({
storeDomain: process.env.SHOPIFY_STORE_DOMAIN!,
apiVersion: "2025-01",
privateAccessToken: process.env.SHOPIFY_STOREFRONT_PRIVATE_TOKEN!,
});Do not modify lib/shopify.ts. Do not make live API calls — the implementation only needs to be structurally correct.
{
"context": "Tests whether the agent uses the correct Shopify Storefront API package, the createStorefrontApiClient factory function, the current API version, and properly distinguishes between public (client-side) and private (server-side) token configurations.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Correct npm package",
"max_score": 15,
"description": "Uses @shopify/storefront-api-client as the npm package (not shopify-buy, @shopify/shopify-api, or any other package)"
},
{
"name": "createStorefrontApiClient factory",
"max_score": 10,
"description": "Uses createStorefrontApiClient() (imported from @shopify/storefront-api-client) to instantiate the client"
},
{
"name": "API version 2025-01",
"max_score": 10,
"description": "Sets apiVersion to \"2025-01\" in the client configuration"
},
{
"name": "publicAccessToken for client-side",
"max_score": 10,
"description": "Uses the publicAccessToken field (not privateAccessToken) in the client-side/browser-facing client configuration"
},
{
"name": "privateAccessToken for server-side",
"max_score": 10,
"description": "Uses the privateAccessToken field (not publicAccessToken) in the server-side client configuration"
},
{
"name": "Private token env var",
"max_score": 10,
"description": "The private access token is read from a non-NEXT_PUBLIC_ environment variable (e.g. SHOPIFY_STOREFRONT_PRIVATE_TOKEN), ensuring it is NOT exposed to the browser"
},
{
"name": "Public token env var",
"max_score": 5,
"description": "The public access token is read from a NEXT_PUBLIC_ prefixed environment variable (e.g. NEXT_PUBLIC_SHOPIFY_STOREFRONT_TOKEN), making it safe for browser use"
},
{
"name": "Two separate clients",
"max_score": 10,
"description": "Creates two distinct client instances: one for public/client-side use and one for private/server-side use"
},
{
"name": "storeDomain config",
"max_score": 5,
"description": "Passes a storeDomain (the myshopify.com domain) to the client configuration"
},
{
"name": "Server-side data fetch uses private client",
"max_score": 15,
"description": "Any server-side data fetching (e.g. getStaticProps, React Server Component, route handler) uses the private/server client instance, not the public one"
}
]
}
Headless Shopify Data Layer Setup
Problem/Feature Description
A growing DTC brand called Pebble & Pine has decided to move their Shopify store to a custom Next.js 14 frontend. Their engineering team has built the UI components but hasn't yet wired up live Shopify data. The team lead has asked you to create the foundational data-fetching layer: a set of TypeScript modules that connect to Shopify's Storefront API and can be imported by both server components and client-side hooks.
The architecture needs to account for two very different contexts: server components and API route handlers that run on Node.js, and interactive client-side widgets running in the browser. These contexts have different performance and security requirements, so the Shopify connection should be configured differently for each.
Output Specification
Produce a working TypeScript implementation with the following files:
lib/shopify.ts— The Shopify client configuration module (exported clients for different contexts)lib/products.ts— A module that exports at least one function to fetch a list of products from Shopify, intended for use in a server contextREADME.md— Brief documentation explaining which client to use in which context and why
Use environment variables for all secrets and store credentials. You can assume the following environment variables will be available at runtime:
NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN— the store's myshopify.com domain (safe for browser)NEXT_PUBLIC_SHOPIFY_STOREFRONT_TOKEN— the public storefront access token (safe for browser)SHOPIFY_STORE_DOMAIN— the store's myshopify.com domain (server only)SHOPIFY_STOREFRONT_PRIVATE_TOKEN— a private storefront access token (server only, NOT safe for browser)
Do not actually call the API (no live credentials exist); the implementation only needs to be structurally correct and ready to use.
{
"name": "finsi/shopify-storefront-api",
"version": "0.1.0",
"summary": "Storefront API queries for headless builds with buy SDK",
"skills": {
"shopify-storefront-api": {
"path": "SKILL.md"
}
}
}