Query the Storefront API
Hydrogen's Storefront API client is a typed GraphQL client for the Storefront API. It works anywhere you can call fetch, so you can use it in loaders, server components, route handlers, middleware, and background jobs in any framework. This guide shows how to choose a client type, create a client, and query the Storefront API.
Anchor to How it worksHow it works
createStorefrontClient() takes three inputs:
type: Which token the client uses, and whether it forwards buyer IP.requestContext: The Shopify request context for the current request, created withcreateShopifyRequestContext(). It carries the request, the resolved market, and the buyer IP. To learn more, refer to The request context.config: Your store domain, token, and optional settings likestorefrontId,cache,waitUntil, and a customfetch.
Queries go through client.graphql(query, {variables}). There are no typed helper methods like client.product(). You write normal Storefront API queries with gql(), and your editor infers the result type from the query.
Hydrogen's cart, predictive search, and request handlers all accept the same client, so one client per request serves your whole server.
Anchor to RequirementsRequirements
- Complete the Deploy a template tutorial, or install
@shopify/hydrogenin a server-rendered project. - To query a real store, get a public or private Storefront API token from the Headless channel or the Hydrogen channel. You don't need a token to query mock.shop.
Anchor to Step 1: Choose a client typeStep 1: Choose a client type
Pick the client type based on where the code runs, which token is safe there, and whether you have a customer's request:
| Type | Token | Use it for |
|---|---|---|
"public" | Public token, or no token | Browser-safe reads, mock.shop, and the default scaffold before you have tokens. |
"private" | Private token, with a trusted buyer IP | Server-rendered requests on behalf of a customer, like the cart and personalized pages. |
"private_no_buyer_context" | Private token, without a buyer IP | Catalog reads that are the same for every customer, like prerendering, static generation, sitemaps, and webhooks. |
A public client accepts undefined as its token, which means tokenless access. When you move to a real store, use a token-backed client: product tags, metaobjects, metafields, menus, and customer data require a token.
Private tokens are server-only. createStorefrontClient() throws if you create a private client in the browser.
Anchor to Use mock.shop during developmentUse mock. shop during development
mock.shop is a public Storefront API with demo data. It's a catalog of stores, each on its own host. The store at mock.shop sells apparel, and the directory at mock.shop/llms.txt lists the others, like pets.mock.shop.
To use mock.shop, create a tokenless public client and set the store domain to a mock.shop host:
mock.shop rejects private tokens. Carts work and checkout is mocked; the Customer Account API isn't available.
Anchor to Step 2: Create a clientStep 2: Create a client
Create the client in server-only code. Create a client that uses a customer's request, like a "private" client or any client with the incoming request's headers, once per request. You can create a client with a static request context, like a "private_no_buyer_context" client for catalog reads, once at module scope.
Create a request-scoped Storefront API client
lib/storefront.tsenv stands for your server environment. Read the variables with your framework's environment API, like process.env on Node or the env binding on Oxygen and Cloudflare Workers. To cache queries, also pass a cache store and waitUntil in config. Refer to Caching.
Call createRequestStorefrontClient() once per request from your app's request entry point. Then share the client with your page loaders through request-scoped context. Run Hydrogen's request handlers in the same entry point, and apply the response headers at your framework's response boundary. For where this code goes in React Router and Next.js, refer to Framework wiring.
In Next.js, Server Components receive headers instead of a Request. createShopifyRequestContext() needs only the request headers, so pass {headers} as the request. Wrap the client in React's cache() so every component in one render shares it.
Anchor to Create a client for catalog readsCreate a client for catalog reads
Catalog pages that serve the same data to every customer don't need buyer context. A module-scoped "private_no_buyer_context" client with a static request context doesn't read request headers. In Next.js, that keeps the page statically renderable.
Module-scoped catalog client
lib/storefront-static.tsAll requests through a client without buyer context share one rate limit bucket. That's fine for catalog data, but use a request-scoped "private" client for the cart and anything personalized.
In Next.js, use both clients: the static client for catalog reads, and the request-scoped client for the cart.
Anchor to Step 3: Pass a trusted buyer IPStep 3: Pass a trusted buyer IP
A "private" client forwards trusted buyer context, including the buyer's IP address, to the Storefront API with each request. Hydrogen doesn't guess which header holds the IP address. Your app reads it from a header that your hosting provider sets and that customers can't spoof.
| Provider | Header |
|---|---|
| Oxygen | oxygen-buyer-ip |
| Cloudflare | CF-Connecting-IP |
| Vercel | x-forwarded-for |
| Fly.io | Fly-Client-IP |
On Fly.io, parse X-Forwarded-For only when another reverse proxy sits in front of your app.
The following helper reads the first address from x-forwarded-for, which Vercel sets. It falls back to a loopback address during local development and throws in production when the header is missing:
Read the buyer IP
lib/buyer-ip.tsPass the buyer IP to createShopifyRequestContext(), not to the client config. Hydrogen's Storefront API proxy and your own client.graphql() calls both read buyer identity from the request context.
Anchor to Step 4: Query dataStep 4: Query data
Write queries with gql() from @shopify/hydrogen, then run them with client.graphql(). Keep gql() calls in server modules and route modules. If a gql() call is reachable from browser code, then bundlers include it in the browser bundle.
Query a product
lib/product.tsCall loadProduct() from your page's server data loader, then render the result:
Render the product
lib/render-product.ts
Vanilla JavaScript
import type {Product} from './product';
export function renderProduct(root: HTMLElement, product: Product) {
const heading = document.createElement('h1');
heading.textContent = product?.title ?? 'Product not found';
root.replaceChildren(heading);
}React
import type {Product} from '~/lib/product';
export function ProductTitle({product}: {product: Product}) {
return <h1>{product?.title ?? 'Product not found'}</h1>;
}Vue
<script setup lang="ts">
import type {Product} from '~/lib/product';
defineProps<{product: Product}>();
</script>
<template>
<h1>{{ product?.title ?? 'Product not found' }}</h1>
</template>data is typed from the query. To set up editor validation and derive types for fragments and variables, refer to Typed queries and validation.
To cache catalog queries, refer to Caching.
Anchor to Step 5: Localize queriesStep 5: Localize queries
The Storefront API returns prices, availability, and translated content for a market when a query uses the @inContext directive. Hydrogen fills in the $country and $language variables from the request context's i18n whenever your query declares them. You pass only the variables you own:
Hydrogen doesn't add @inContext for you. If your query doesn't declare $country and $language and use them in @inContext, then the Storefront API returns the store's default market. The request context's country and language override values in variables.
To resolve the market for each request, refer to Markets.
Anchor to Step 6: Handle errorsStep 6: Handle errors
Transport errors throw, and the client returns GraphQL errors:
- Thrown: Non-2xx responses, network failures, and JSON parse errors throw
StorefrontApiError. Timeouts throwStorefrontTimeoutError, which extendsStorefrontApiError. - Returned: A 200 response with a GraphQL
errorsarray doesn't throw, because partial success is valid in GraphQL. Checkerrorsalongsidedata.
Every successful result includes headers, the Headers object from the Storefront API response.
Queries with missing or renamed fields can pass tsc but fail at runtime, often looking like empty results. On a product page, that can look like a missing product and turn into a misleading 404. To catch these errors before you deploy, validate your queries in CI.
Anchor to Step 7: Test your clientStep 7: Test your client
To test code that uses the client, pass a mock fetch in the client config. The client calls it instead of the global fetch: