next-shopify-experimental
v0.0.3
Published
Experimental Shopify toolkit for Next.js: a typed Storefront API client, automatic cache tagging, and webhook-driven revalidation.
Maintainers
Readme
next-shopify-experimental
A Shopify toolkit for Next.js: a typed Storefront API client with built in support for Next.js 16 Cache Components.
Requirements
- Next.js 16+ with Cache Components enabled (
cacheComponents: true) - React 19
- A Shopify store with a Storefront API access token, plus a webhook signing secret for revalidation
Installation
npm install next-shopify-experimental
# or
pnpm add next-shopify-experimental
# or
yarn add next-shopify-experimental
# or
bun add next-shopify-experimentalQuick start
1. Enable Cache Components
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig2. Create the client and a cached fetch
// lib/shopify.ts
import { createStorefrontClient } from 'next-shopify-experimental'
import { defineFetch } from 'next-shopify-experimental/cache'
// Reads SHOPIFY_STORE_DOMAIN + SHOPIFY_STOREFRONT_TOKEN from the environment.
export const { shopifyFetch } = defineFetch({
client: createStorefrontClient(),
})createStorefrontClient() reads your store domain and access token from the environment by convention, so the common case takes no arguments. Pass any option to override its env default, and it validates up front — a missing domain or token throws a clear, actionable error instead of failing cryptically on the first request.
3. Read data inside a 'use cache' boundary
shopifyFetch runs the query, then tags the surrounding cache entry with the GID of every resource it returned. Call it from a 'use cache' function:
// app/page.tsx
import { shopifyFetch } from '@/lib/shopify'
async function getShop() {
'use cache'
const { data } = await shopifyFetch<{ shop: { id: string; name: string } }>({
query: `{ shop { id name } }`,
})
return data?.shop
}
export default async function Page() {
const shop = await getShop()
return <h1>{shop?.name}</h1>
}4. Revalidate on change with a webhook
Add a webhook route. When Shopify reports a change, next-shopify-experimental verifies it and revalidates the matching tags:
// app/api/shopify/webhook/route.ts
export { POST } from 'next-shopify-experimental/webhook'Then create a webhook subscription pointed at https://your-app.com/api/shopify/webhook, and set its signing secret as SHOPIFY_WEBHOOK_SECRET.
5. Set environment variables
# .env.local
SHOPIFY_STORE_DOMAIN=your-shop.myshopify.com
SHOPIFY_STOREFRONT_TOKEN=your-public-storefront-access-token
SHOPIFY_WEBHOOK_SECRET=your-webhook-signing-secret| Variable | Read by | Description |
| -------------------------- | ------------------------ | ---------------------------------------------------------- |
| SHOPIFY_STORE_DOMAIN | createStorefrontClient | Your store's domain. |
| SHOPIFY_STOREFRONT_TOKEN | createStorefrontClient | A public Storefront API access token. |
| SHOPIFY_WEBHOOK_SECRET | the webhook handler | The webhook signing secret — read automatically by POST. |
createStorefrontClient also reads SHOPIFY_STOREFRONT_PRIVATE_TOKEN (a server-only token, as an alternative to the public one) and SHOPIFY_API_VERSION (optional; otherwise a built-in default is used).
That's the whole integration: reads are cached and tagged, and a shop/update webhook busts exactly the pages that read the shop.
How it works
Every object in Shopify's API has a global ID — a GID, like gid://shopify/Shop/1. next-shopify-experimental uses it as the link between reads and writes:
- On read,
shopifyFetchscans the response for GIDs and tags the cache entry with each one (shopify:shop:1). - On write, your webhook receives the same GID, resolves it to the same tag, and calls
revalidateTag.
Because both sides derive the tag from the same GID, a change invalidates exactly the cache entries that read that resource — with no tag bookkeeping on your part.
API reference
createStorefrontClient
Creates the Storefront API client, reading configuration from the environment by convention so the common case is zero-argument. It validates up front — a missing store domain or access token throws a clear, actionable error rather than failing cryptically on the first request. Pass any option to override its env default.
import { createStorefrontClient } from 'next-shopify-experimental'
// Reads SHOPIFY_STORE_DOMAIN + SHOPIFY_STOREFRONT_TOKEN from the environment:
const client = createStorefrontClient()
// …or override any option:
const client = createStorefrontClient({ apiVersion: '2025-10' })Every option is optional; each falls back to an environment variable:
| Option | Falls back to | Description |
| -------------------- | ---------------------------------- | ------------------------------------------------------- |
| storeDomain | SHOPIFY_STORE_DOMAIN | Your *.myshopify.com (or custom) domain. |
| publicAccessToken | SHOPIFY_STOREFRONT_TOKEN | A public Storefront access token (safe in the browser). |
| privateAccessToken | SHOPIFY_STOREFRONT_PRIVATE_TOKEN | A private Storefront access token (server only). |
| apiVersion | SHOPIFY_API_VERSION (or default) | Storefront API version, e.g. '2026-04'. |
A store domain and exactly one access token are required (from config or the environment). It also accepts the client's remaining options (clientName, retries, customFetchApi, logger).
createStorefrontApiClient
The raw client factory, re-exported from @shopify/storefront-api-client (along with all of its types) unchanged — no env convention, no validation. Reach for it when you want full, unmanaged control; otherwise prefer createStorefrontClient. request() is return-based and does not throw on GraphQL errors.
import { createStorefrontApiClient } from 'next-shopify-experimental'
const client = createStorefrontApiClient({
storeDomain: 'your-shop.myshopify.com',
apiVersion: '2026-04',
publicAccessToken: process.env.SHOPIFY_STOREFRONT_TOKEN!,
})defineFetch
From next-shopify-experimental/cache. Binds a client to a shopifyFetch that tags reads for revalidation.
import { defineFetch } from 'next-shopify-experimental/cache'
export const { shopifyFetch } = defineFetch({ client })| Name | Type | Default | Description |
| -------- | --------------------- | ------- | ------------------------------------------------------------ |
| client | StorefrontApiClient | — | The client returned by createStorefrontApiClient. |
| debug | boolean | false | Log each fetch's collected cache tags to the server console. |
shopifyFetch
Returns the client's { data, errors, extensions } response, and tags the surrounding 'use cache' entry.
const { data, errors } = await shopifyFetch<TData>({ query, variables })| Name | Type | Default | Description |
| ----------- | --------- | --------- | ------------------------------------------------------------------------------------------- |
| query | string | — | The GraphQL operation. |
| variables | object | — | Operation variables. |
| cache | boolean | true | Tag this read. Set false for a dynamic, uncached read — no 'use cache' boundary needed. |
| debug | boolean | inherited | Override the defineFetch debug setting for this call. |
It also accepts the underlying client's request options (apiVersion, headers, retries, signal).
Inside a
'use cache'boundary,shopifyFetchtags the entry. Called outside one withoutcache: false, it throws with a message pointing you at both fixes.
next-shopify-experimental/webhook
POST
The convention handler — verifies with SHOPIFY_WEBHOOK_SECRET and revalidates. Re-export it as your route:
// app/api/shopify/webhook/route.ts
export { POST } from 'next-shopify-experimental/webhook'createWebhookHandler
Use the factory when you need to configure it:
import { createWebhookHandler } from 'next-shopify-experimental/webhook'
export const POST = createWebhookHandler({
secret: process.env.SHOPIFY_WEBHOOK_SECRET,
})| Name | Type | Default | Description |
| ----------------------- | --------- | ------------------------------------ | -------------------------------------------------------------------------------------- |
| secret | string | process.env.SHOPIFY_WEBHOOK_SECRET | Webhook signing secret. The handler fails closed (500) if it's missing. |
| signatureVerification | boolean | true | Set false only if you verify upstream — HMAC is skipped and no secret is required. |
Responses: 200 { revalidated } for a verified webhook (including topics it doesn't tag), 401 for a missing or invalid signature, 500 if the secret isn't configured.
Primitives
To build a fully custom handler, compose these directly:
| Export | Description |
| -------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| verifyWebhook({ body, signature, secret }) | Verify a Shopify webhook signature (Web Crypto, constant-time). Resolves a boolean; never throws. |
| getWebhookCacheTags(topic, payload) | Derive the cache tags for a verified payload — the same tags the read side produced. |
Supported resources
next-shopify-experimental tags the Shop resource today. Other resources (products, collections, …) aren't tagged yet: their data still reads normally, but no tags are emitted for them and their webhooks are acknowledged without revalidation — no errors are thrown. Coverage grows as the resource registry expands.
Local example
A runnable Next.js app lives in apps/example.
