Storefront API
How the template calls Shopify's Storefront GraphQL API - the fetch client, query patterns, caching, and error handling.
All Shopify data flows through a single shopifyFetch function in lib/shopify/client.ts. It wraps the native fetch API with Shopify authentication, variable hashing for cache keys, and error handling.
For Shopify admin setup and required token scopes, see Storefront API Permissions.
shopifyFetch
The function accepts three fields:
- operation - the GraphQL operation name. Must match the name in the query string. Appended to the request URL as a query parameter for cache key differentiation.
- query - the full GraphQL query or mutation as a template string, including any fragment interpolations.
- variables - an optional object passed as JSON in the request body. Never interpolate values into the query string directly.
The return type is generic - pass the expected shape of data from the GraphQL response as the type parameter.
Request details
Every request is a POST to https://{SHOPIFY_STORE_DOMAIN}/api/2025-01/graphql.json with headers:
| Header | Value |
|---|---|
Content-Type | application/json |
X-Shopify-Storefront-Access-Token | Your public storefront token |
Accept-Encoding | gzip, deflate |
The URL includes query parameters for operation and a 12-character SHA-1 hash of the serialized variables. This ensures Next.js treats requests with different variables as distinct cache entries, even though they hit the same endpoint.
Writing a query
Define the query as a template string, interpolating shared fragments at the top:
The @inContext directive passes country and language for localized pricing and content. Use getCountryCode(locale) and getLanguageCode(locale) from lib/i18n to extract these from a locale string.
Always verify fields against the live Storefront API schema with shopify-ai-toolkit or /vercel-shop:shopify-graphql-reference when adding or modifying queries - don't guess field names.
Fragments
Shared fragments in lib/shopify/fragments.ts avoid repeating field selections:
| Fragment | Covers |
|---|---|
MONEY_FRAGMENT | amount and currencyCode on MoneyV2 |
IMAGE_FRAGMENT | url, altText, width, height on Image |
PRODUCT_VARIANT_FRAGMENT | Variant price, options, image, availability |
PRODUCT_FRAGMENT | Full product: media, variants (up to 50), options with swatches, metafields, price ranges, SEO |
CATEGORY_PRODUCT_FRAGMENT | Lightweight product for listing pages (fewer fields) |
Embed fragments in a query with template literal interpolation: `${PRODUCT_FRAGMENT} query ...`.
Writing an operation function
Operation functions live in lib/shopify/operations/ and follow a consistent pattern:
Key elements:
"use cache: remote"- opts the function into Next.js request-level cachingcacheLife("max")- caches indefinitely until manually revalidatedcacheTag(...)- assigns tags for granular invalidation viarevalidateTag()- Transform - convert the Shopify response to a domain type before returning. Components never import Shopify types directly.
Caching
All read operations use "use cache: remote" with cacheLife("max") and one or more cache tags. Tags follow a hierarchy:
| Tag pattern | Scope |
|---|---|
products | All product queries |
product-{handle} | Single product by handle |
product-{numericId} | Single product by Shopify numeric ID |
recommendations-{handle} | Product recommendations |
collections | All collection queries |
collection-{handle} | Single collection |
menus | Navigation menus |
cart | All cart operations |
Invalidate with revalidateTag("product-blue-tee") or updateTag("cart") in server actions. Cart mutations must always call invalidateCartCache() from lib/cart-cache - this is a hard requirement.
Mutations
Cart mutations use the same shopifyFetch function but are not cached. They call Shopify cartLinesAdd, cartLinesUpdate, and cartLinesRemove mutations:
Shopify returns the full updated cart in every mutation response, so there's no need for a follow-up query.
Error handling
The client handles errors at two levels:
HTTP errors - a non-200 response throws immediately with the status code and text.
GraphQL errors - if the response contains errors and no data, the client throws. If there are errors but data is also present (partial success), it logs a warning and returns the partial data.
Individual operations add their own handling on top. For example, getCart() wraps the call in try-catch and returns undefined on failure, while getProduct() throws if the product isn't found.
Debug logging
Set DEBUG_SHOPIFY=true to log every request with timing and a variable preview:
Transforms
Every operation converts the Shopify response to a domain type before returning. Transforms live in lib/shopify/transforms/:
| Transform | Input → Output |
|---|---|
transformShopifyProductDetails | ShopifyProduct → ProductDetails |
transformShopifyCart | ShopifyCart → Cart |
Domain types are defined in lib/types.ts and are provider-agnostic. Components import these types, never the Shopify-specific response shapes. This keeps the UI decoupled from the API layer.
Key files and tools
| Resource | Purpose |
|---|---|
lib/shopify/client.ts | Core fetch wrapper with variable hashing and error handling |
lib/shopify/fragments.ts | Reusable GraphQL fragment definitions |
lib/shopify/operations/products.ts | Product queries: single, batch, search, recommendations |
lib/shopify/operations/cart.ts | Cart queries and mutations |
lib/shopify/operations/collections.ts | Collection queries |
lib/shopify/operations/search.ts | Predictive search |
lib/shopify/operations/menu.ts | Navigation menu queries |
lib/shopify/transforms/product.ts | Shopify product → domain type |
lib/shopify/transforms/cart.ts | Shopify cart → domain type |
lib/cart-cache.ts | invalidateCartCache() for mutation cache busting |
shopify-ai-toolkit | Live Storefront and Customer Account schema inspection |