// Shopify Storefront API access, backed by `@shopify/hydrogen` (preview build). // // The package supplies the transport (`createStorefrontClient`) and the typed // query documents (`gql`, see graphql/*.ts). `shopifyFetch` keeps the call // shape the rest of the app already uses — `{ query, variables }` in, `{ data }` // out, throwing on GraphQL errors — but now infers `data` from the document. import { createShopifyRequestContext, createStorefrontClient, StorefrontApiError, type AnyStorefrontQueryString, type StorefrontApi, } from '@shopify/hydrogen'; const SHOPIFY_STORE_DOMAIN = process.env.NEXT_PUBLIC_SHOPIFY_DOMAIN; const SHOPIFY_STOREFRONT_ACCESS_TOKEN = process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN; const SHOPIFY_API_VERSION = process.env.NEXT_PUBLIC_SHOPIFY_API_VERSION || '2025-07'; const SHOPIFY_STOREFRONT_API_URL = `https://${SHOPIFY_STORE_DOMAIN}/api/${SHOPIFY_API_VERSION}/graphql.json`; // No incoming request and no buyer context: these clients are module-scoped and // serve both server rendering and the browser-side hooks, so they must not close // over per-request state. `$country`/`$language` are injected from this i18n. const requestContext = createShopifyRequestContext({ request: { headers: new Headers() }, i18n: { country: 'US', language: 'EN' }, }); // Hydrogen calls `fetch(url, init, cacheOptions)`; Next's caching hints ride // along on `init`, which is how the two ways of caching get to coexist. const fetchWith = (overrides: RequestInit): typeof globalThis.fetch => ((url, init) => globalThis.fetch(url, { ...init, ...overrides })) as typeof globalThis.fetch; const config = { storeDomain: SHOPIFY_STORE_DOMAIN!, apiVersion: SHOPIFY_API_VERSION, // `mock.shop` and other tokenless storefronts leave this unset. publicStorefrontToken: SHOPIFY_STOREFRONT_ACCESS_TOKEN, }; /** * Default client. Uncached, because carts and customer reads must never serve a * stale response. */ export const storefront = createStorefrontClient({ type: 'public', requestContext, config: { ...config, fetch: fetchWith({ cache: 'no-store' }) }, }); /** * Client for catalogue data that changes rarely (shop policies, and anything * else safe to serve from Next's data cache for an hour). */ export const cachedStorefront = createStorefrontClient({ type: 'public', requestContext, config: { ...config, fetch: fetchWith({ next: { revalidate: 3600 } }) }, }); interface ShopifyFetchArgs { query: TDoc; variables?: StorefrontApi.VariablesOf; /** Serve from Next's data cache instead of hitting Shopify every time. */ cache?: boolean; } /** * Runs a `gql()` document and returns its data. * * Hydrogen returns GraphQL errors rather than throwing them (a 200 with partial * data is still a valid response), so this re-raises them to preserve the * fail-loudly behaviour the callers were written against. Transport failures * already throw as `StorefrontApiError`. */ export async function shopifyFetch({ query, variables, cache = false, }: ShopifyFetchArgs): Promise<{ data: StorefrontApi.ResultOf; }> { const client = cache ? cachedStorefront : storefront; try { const { data, errors } = await client.graphql(query, { variables, } as never); if (errors?.length) { console.error('Shopify API errors:', errors); throw new Error(`Shopify GraphQL errors: ${JSON.stringify(errors)}`); } return { data: data as StorefrontApi.ResultOf }; } catch (error) { if (error instanceof StorefrontApiError) { console.error('Shopify fetch error:', error.toJSON()); } else { console.error('Shopify fetch error:', error); } throw error; } } export { SHOPIFY_STORE_DOMAIN, SHOPIFY_STOREFRONT_API_URL };