Shopify Hydrogen
Overview
Hydrogen is Shopify's official React-based framework for building headless storefronts, built on top of Remix and deployed to Oxygen (Shopify's edge hosting). It provides first-class primitives for the Storefront API — product queries, cart management, customer accounts — alongside Shopify-specific components and hooks that handle caching, streaming, and SEO automatically. This skill covers scaffolding a Hydrogen project, querying the Storefront API, implementing cart functionality, and deploying to Oxygen.
When to Use This Skill
- When building a custom Shopify storefront with full design and UX control
- When the default Shopify Online Store theme is too limiting for your design requirements
- When you need server-side rendering, streaming, and edge-deployed performance
- When integrating third-party services (loyalty, CMS, personalization) directly into the storefront
- When you want a Shopify-managed backend with a completely custom frontend stack
Prerequisites & Platform Notes
This skill is written for custom/headless storefronts (Node.js, Python, or similar backend). The code examples use TypeScript/Node.js and can be adapted to any stack.
Shopify: Shopify Hydrogen is Shopify's headless framework. MACH/composable patterns apply when using Shopify as the commerce backend with a custom frontend, or when mixing Shopify with other best-of-breed services.
WooCommerce: WooCommerce can serve as a headless backend via its REST API and WPGraphQL. These patterns apply when decoupling the frontend from WordPress.
Magento: Magento's GraphQL API and PWA Studio support headless architectures. These composable patterns apply to Magento as a backend service in a MACH stack.
You'll need:
- Node.js 18+ (or adapt to your backend language)
- Redis for caching/queues
- An email sending service (SendGrid, AWS SES, or Postmark)
- CDN (Cloudflare, CloudFront, or Fastly)
Core Instructions
Scaffold a Hydrogen project
npm create @shopify/hydrogen@latest -- --quickstart
# or with options:
npm create @shopify/hydrogen@latest
# Follow prompts: project name, language (TypeScript), mock shop or real credentials
cd my-hydrogen-store
npm run dev
# http://localhost:3000
The project structure follows Remix file-based routing:
app/
routes/
_index.tsx # Homepage
products.$handle.tsx # Product detail page
collections.$handle.tsx
cart.tsx
components/
lib/
fragments.ts # Reusable GraphQL fragments
server.ts # Hydrogen + Remix entry point
Configure Storefront API credentials
Create a Storefront API token in your Shopify admin under Apps → Develop apps → Create an app → Storefront API.
# .env
SESSION_SECRET="your-session-secret"
PUBLIC_STOREFRONT_API_TOKEN="your-public-token"
PUBLIC_STORE_DOMAIN="your-store.myshopify.com"
PUBLIC_STOREFRONT_API_VERSION="2025-01"
The server.ts wires Hydrogen into Remix:
import {createHydrogenContext} from '@shopify/hydrogen';
const hydrogenContext = createHydrogenContext({
storefront: {
apiVersion: env.PUBLIC_STOREFRONT_API_VERSION,
privateStorefrontToken: env.PRIVATE_STOREFRONT_API_TOKEN,
publicStorefrontToken: env.PUBLIC_STOREFRONT_API_TOKEN,
storeDomain: env.PUBLIC_STORE_DOMAIN,
},
session: HydrogenSession.init(request, [env.SESSION_SECRET]),
});
Query the Storefront API
Hydrogen provides a storefront.query method with built-in caching policies.
// app/routes/products.$handle.tsx
import {useLoaderData} from '@remix-run/react';
import {json, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
export async function loader({params, context}: LoaderFunctionArgs) {
const {storefront} = context;
const {product} = await storefront.query(PRODUCT_QUERY, {
variables: {handle: params.handle},
cache: storefront.CacheLong(), // Cache at CDN for 24h
});
if (!product) throw new Response('Not Found', {status: 404});
return json({product});
}
const PRODUCT_QUERY = `#graphql
query Product($handle: String!) {
product(handle: $handle) {
id
title
descriptionHtml
featuredImage { url altText width height }
variants(first: 20) {
nodes {
id
title
price { amount currencyCode }
availableForSale
selectedOptions { name value }
}
}
}
}
` as const;
Implement cart with Hydrogen cart utilities
Hydrogen provides server-side cart actions via Remix action functions:
// app/routes/cart.tsx
import {CartForm} from '@shopify/hydrogen';
import type {ActionFunctionArgs} from '@shopify/remix-oxygen';
export async function action({request, context}: ActionFunctionArgs) {
const {cart} = context;
const formData = await request.formData();
const {action, inputs} = CartForm.getFormInput(formData);
let result;
switch (action) {
case CartForm.ACTIONS.LinesAdd:
result = await cart.addLines(inputs.lines);
break;
case CartForm.ACTIONS.LinesUpdate:
result = await cart.updateLines(inputs.lines);
break;
case CartForm.ACTIONS.LinesRemove:
result = await cart.removeLines(inputs.lineIds);
break;
default:
throw new Error(`Unhandled cart action: ${action}`);
}
const headers = cart.setCartId(result.cart.id);
return json(result, {headers});
}
// Add to cart form component
export function AddToCartButton({variantId}: {variantId: string}) {
return (
<CartForm
route="/cart"
action={CartForm.ACTIONS.LinesAdd}
inputs={{lines: [{merchandiseId: variantId, quantity: 1}]}}
>
<button type="submit">Add to Cart</button>
</CartForm>
);
}
Use Hydrogen caching strategies
Hydrogen exposes named caching strategies that map to CDN cache-control headers:
// Long cache for static catalog data
const {collections} = await storefront.query(COLLECTIONS_QUERY, {
cache: storefront.CacheLong(), // s-maxage=3600, stale-while-revalidate=82800
});
// Short cache for inventory-sensitive data
const {product} = await storefront.query(PRODUCT_WITH_INVENTORY, {
cache: storefront.CacheShort(), // s-maxage=1, stale-while-revalidate=9
});
// No cache for personalized/cart data
const {customer} = await storefront.query(CUSTOMER_QUERY, {
cache: storefront.CacheNone(),
});
// Custom strategy
const {data} = await storefront.query(QUERY, {
cache: storefront.CacheCustom({
mode: 'public',
maxAge: 600,
staleWhileRevalidate: 3000,
}),
});
Deploy to Oxygen
npm install -g @shopify/cli
shopify hydrogen deploy
# Creates a deployment in your Shopify admin under Online Store → Themes → Headless
For CI/CD, use the GitHub Action:
# .github/workflows/oxygen.yml
- uses: Shopify/hydrogen-action@v1
with:
shop: ${{ secrets.SHOPIFY_SHOP_DOMAIN }}
token: ${{ secrets.SHOPIFY_CLI_TOKEN }}
Examples
Collection page with filtering and sorting
// app/routes/collections.$handle.tsx
export async function loader({params, request, context}: LoaderFunctionArgs) {
const {storefront} = context;
const url = new URL(request.url);
const sortKey = url.searchParams.get('sort') as ProductCollectionSortKeys | null;
const {collection} = await storefront.query(COLLECTION_QUERY, {
variables: {
handle: params.handle,
first: 24,
sortKey: sortKey ?? 'BEST_SELLING',
reverse: sortKey === 'PRICE' ? false : true,
},
cache: storefront.CacheShort(),
});
return json({collection});
}
const COLLECTION_QUERY = `#graphql
query Collection(
$handle: String!
$first: Int
$sortKey: ProductCollectionSortKeys
$reverse: Boolean
) {
collection(handle: $handle) {
id
title
description
image { url altText }
products(first: $first, sortKey: $sortKey, reverse: $reverse) {
nodes {
id
title
handle
priceRange { minVariantPrice { amount currencyCode } }
featuredImage { url altText }
}
pageInfo { hasNextPage endCursor }
}
}
}
` as const;
Customer authentication with new Customer Account API
// Hydrogen supports the new Customer Account API (OAuth-based)
// app/lib/customer-account.server.ts
export async function loader({context}: LoaderFunctionArgs) {
const {customerAccount} = context;
const isLoggedIn = await customerAccount.isLoggedIn();
if (!isLoggedIn) {
return redirect('/account/login');
}
const {data} = await customerAccount.query(`#graphql
query Customer {
customer {
id
firstName
lastName
emailAddress { emailAddress }
orders(first: 10) {
nodes {
id
number
processedAt
financialStatus
totalPrice { amount currencyCode }
}
}
}
}
`);
return json({customer: data.customer});
}
Best Practices
- Use
storefront.CacheLong() for catalog data — product and collection data rarely changes; long cache TTLs dramatically improve TTFB on Oxygen's edge network
- Colocate GraphQL queries with routes — define
as const fragment strings in the same file as the loader; this keeps data requirements visible and enables TypeScript inference
- Use Hydrogen's
<Image> and <Money> components — they handle Shopify CDN image optimization URLs and currency formatting automatically
- Leverage Remix defer + Suspense for non-critical data — render the product immediately and stream recommendations or reviews with
defer()
- Keep cart state server-side via cookies — Hydrogen's cart utilities store the cart ID in a signed cookie; avoid client-only cart state that breaks SSR
- Use the Storefront API's
@inContext directive — pass language and country context to get localized prices and translated content per request
- Pin the Storefront API version in
.env — Shopify deprecates old API versions; explicit pinning prevents surprise breakage on API updates
Common Pitfalls
| Problem |
Solution |
| "Storefront API token not authorized" |
Ensure the token has unauthenticated_read_* scopes; private tokens are only for server-side requests |
| Cart state lost between page navigations |
Store cart ID in the session cookie using cart.setCartId(); never store cart ID in component state |
| Images not optimized on Oxygen |
Use Hydrogen's <Image> component or the getImageData() helper to append Shopify CDN transform params |
| TypeScript errors on GraphQL queries |
Run npm run codegen to regenerate types after changing queries; queries must be tagged as const |
| Deployment fails with "missing environment variables" |
Oxygen env vars must be added in the Shopify admin under the Hydrogen deployment settings, not just in .env |
Related Skills
- @saleor-development
- @jamstack-storefront
- @composable-commerce
- @pwa-storefront
- @commerce-api-gateway
1---2name: shopify-hydrogen3description: Build a custom Shopify storefront using the Hydrogen React framework with Remix routing and deploy it to Shopify's Oxygen edge hosting4---56# Shopify Hydrogen78## Overview910Hydrogen is Shopify's official React-based framework for building headless storefronts, built on top of Remix and deployed to Oxygen (Shopify's edge hosting). It provides first-class primitives for the Storefront API — product queries, cart management, customer accounts — alongside Shopify-specific components and hooks that handle caching, streaming, and SEO automatically. This skill covers scaffolding a Hydrogen project, querying the Storefront API, implementing cart functionality, and deploying to Oxygen.1112## When to Use This Skill1314- When building a custom Shopify storefront with full design and UX control15- When the default Shopify Online Store theme is too limiting for your design requirements16- When you need server-side rendering, streaming, and edge-deployed performance17- When integrating third-party services (loyalty, CMS, personalization) directly into the storefront18- When you want a Shopify-managed backend with a completely custom frontend stack1920## Prerequisites & Platform Notes2122**This skill is written for custom/headless storefronts** (Node.js, Python, or similar backend). The code examples use TypeScript/Node.js and can be adapted to any stack.2324**Shopify**: Shopify Hydrogen is Shopify's headless framework. MACH/composable patterns apply when using Shopify as the commerce backend with a custom frontend, or when mixing Shopify with other best-of-breed services.25**WooCommerce**: WooCommerce can serve as a headless backend via its REST API and WPGraphQL. These patterns apply when decoupling the frontend from WordPress.26**Magento**: Magento's GraphQL API and PWA Studio support headless architectures. These composable patterns apply to Magento as a backend service in a MACH stack.2728**You'll need**:29- Node.js 18+ (or adapt to your backend language)30- Redis for caching/queues31- An email sending service (SendGrid, AWS SES, or Postmark)32- CDN (Cloudflare, CloudFront, or Fastly)3334## Core Instructions35361. **Scaffold a Hydrogen project**3738 ```bash39 npm create @shopify/hydrogen@latest -- --quickstart40 # or with options:41 npm create @shopify/hydrogen@latest42 # Follow prompts: project name, language (TypeScript), mock shop or real credentials43 cd my-hydrogen-store44 npm run dev45 # http://localhost:300046 ```4748 The project structure follows Remix file-based routing:49 ```50 app/51 routes/52 _index.tsx # Homepage53 products.$handle.tsx # Product detail page54 collections.$handle.tsx55 cart.tsx56 components/57 lib/58 fragments.ts # Reusable GraphQL fragments59 server.ts # Hydrogen + Remix entry point60 ```61622. **Configure Storefront API credentials**6364 Create a Storefront API token in your Shopify admin under **Apps → Develop apps → Create an app → Storefront API**.6566 ```bash67 # .env68 SESSION_SECRET="your-session-secret"69 PUBLIC_STOREFRONT_API_TOKEN="your-public-token"70 PUBLIC_STORE_DOMAIN="your-store.myshopify.com"71 PUBLIC_STOREFRONT_API_VERSION="2025-01"72 ```7374 The `server.ts` wires Hydrogen into Remix:75 ```typescript76 import {createHydrogenContext} from '@shopify/hydrogen';7778 const hydrogenContext = createHydrogenContext({79 storefront: {80 apiVersion: env.PUBLIC_STOREFRONT_API_VERSION,81 privateStorefrontToken: env.PRIVATE_STOREFRONT_API_TOKEN,82 publicStorefrontToken: env.PUBLIC_STOREFRONT_API_TOKEN,83 storeDomain: env.PUBLIC_STORE_DOMAIN,84 },85 session: HydrogenSession.init(request, [env.SESSION_SECRET]),86 });87 ```88893. **Query the Storefront API**9091 Hydrogen provides a `storefront.query` method with built-in caching policies.9293 ```typescript94 // app/routes/products.$handle.tsx95 import {useLoaderData} from '@remix-run/react';96 import {json, type LoaderFunctionArgs} from '@shopify/remix-oxygen';9798 export async function loader({params, context}: LoaderFunctionArgs) {99 const {storefront} = context;100 const {product} = await storefront.query(PRODUCT_QUERY, {101 variables: {handle: params.handle},102 cache: storefront.CacheLong(), // Cache at CDN for 24h103 });104105 if (!product) throw new Response('Not Found', {status: 404});106 return json({product});107 }108109 const PRODUCT_QUERY = `#graphql110 query Product($handle: String!) {111 product(handle: $handle) {112 id113 title114 descriptionHtml115 featuredImage { url altText width height }116 variants(first: 20) {117 nodes {118 id119 title120 price { amount currencyCode }121 availableForSale122 selectedOptions { name value }123 }124 }125 }126 }127 ` as const;128 ```1291304. **Implement cart with Hydrogen cart utilities**131132 Hydrogen provides server-side cart actions via Remix action functions:133134 ```typescript135 // app/routes/cart.tsx136 import {CartForm} from '@shopify/hydrogen';137 import type {ActionFunctionArgs} from '@shopify/remix-oxygen';138139 export async function action({request, context}: ActionFunctionArgs) {140 const {cart} = context;141 const formData = await request.formData();142 const {action, inputs} = CartForm.getFormInput(formData);143144 let result;145 switch (action) {146 case CartForm.ACTIONS.LinesAdd:147 result = await cart.addLines(inputs.lines);148 break;149 case CartForm.ACTIONS.LinesUpdate:150 result = await cart.updateLines(inputs.lines);151 break;152 case CartForm.ACTIONS.LinesRemove:153 result = await cart.removeLines(inputs.lineIds);154 break;155 default:156 throw new Error(`Unhandled cart action: ${action}`);157 }158159 const headers = cart.setCartId(result.cart.id);160 return json(result, {headers});161 }162163 // Add to cart form component164 export function AddToCartButton({variantId}: {variantId: string}) {165 return (166 <CartForm167 route="/cart"168 action={CartForm.ACTIONS.LinesAdd}169 inputs={{lines: [{merchandiseId: variantId, quantity: 1}]}}170 >171 <button type="submit">Add to Cart</button>172 </CartForm>173 );174 }175 ```1761775. **Use Hydrogen caching strategies**178179 Hydrogen exposes named caching strategies that map to CDN cache-control headers:180181 ```typescript182 // Long cache for static catalog data183 const {collections} = await storefront.query(COLLECTIONS_QUERY, {184 cache: storefront.CacheLong(), // s-maxage=3600, stale-while-revalidate=82800185 });186187 // Short cache for inventory-sensitive data188 const {product} = await storefront.query(PRODUCT_WITH_INVENTORY, {189 cache: storefront.CacheShort(), // s-maxage=1, stale-while-revalidate=9190 });191192 // No cache for personalized/cart data193 const {customer} = await storefront.query(CUSTOMER_QUERY, {194 cache: storefront.CacheNone(),195 });196197 // Custom strategy198 const {data} = await storefront.query(QUERY, {199 cache: storefront.CacheCustom({200 mode: 'public',201 maxAge: 600,202 staleWhileRevalidate: 3000,203 }),204 });205 ```2062076. **Deploy to Oxygen**208209 ```bash210 npm install -g @shopify/cli211 shopify hydrogen deploy212 # Creates a deployment in your Shopify admin under Online Store → Themes → Headless213 ```214215 For CI/CD, use the GitHub Action:216 ```yaml217 # .github/workflows/oxygen.yml218 - uses: Shopify/hydrogen-action@v1219 with:220 shop: ${{ secrets.SHOPIFY_SHOP_DOMAIN }}221 token: ${{ secrets.SHOPIFY_CLI_TOKEN }}222 ```223224## Examples225226### Collection page with filtering and sorting227228```typescript229// app/routes/collections.$handle.tsx230export async function loader({params, request, context}: LoaderFunctionArgs) {231 const {storefront} = context;232 const url = new URL(request.url);233 const sortKey = url.searchParams.get('sort') as ProductCollectionSortKeys | null;234235 const {collection} = await storefront.query(COLLECTION_QUERY, {236 variables: {237 handle: params.handle,238 first: 24,239 sortKey: sortKey ?? 'BEST_SELLING',240 reverse: sortKey === 'PRICE' ? false : true,241 },242 cache: storefront.CacheShort(),243 });244245 return json({collection});246}247248const COLLECTION_QUERY = `#graphql249 query Collection(250 $handle: String!251 $first: Int252 $sortKey: ProductCollectionSortKeys253 $reverse: Boolean254 ) {255 collection(handle: $handle) {256 id257 title258 description259 image { url altText }260 products(first: $first, sortKey: $sortKey, reverse: $reverse) {261 nodes {262 id263 title264 handle265 priceRange { minVariantPrice { amount currencyCode } }266 featuredImage { url altText }267 }268 pageInfo { hasNextPage endCursor }269 }270 }271 }272` as const;273```274275### Customer authentication with new Customer Account API276277```typescript278// Hydrogen supports the new Customer Account API (OAuth-based)279// app/lib/customer-account.server.ts280export async function loader({context}: LoaderFunctionArgs) {281 const {customerAccount} = context;282 const isLoggedIn = await customerAccount.isLoggedIn();283284 if (!isLoggedIn) {285 return redirect('/account/login');286 }287288 const {data} = await customerAccount.query(`#graphql289 query Customer {290 customer {291 id292 firstName293 lastName294 emailAddress { emailAddress }295 orders(first: 10) {296 nodes {297 id298 number299 processedAt300 financialStatus301 totalPrice { amount currencyCode }302 }303 }304 }305 }306 `);307308 return json({customer: data.customer});309}310```311312## Best Practices313314- **Use `storefront.CacheLong()` for catalog data** — product and collection data rarely changes; long cache TTLs dramatically improve TTFB on Oxygen's edge network315- **Colocate GraphQL queries with routes** — define `as const` fragment strings in the same file as the loader; this keeps data requirements visible and enables TypeScript inference316- **Use Hydrogen's `<Image>` and `<Money>` components** — they handle Shopify CDN image optimization URLs and currency formatting automatically317- **Leverage Remix defer + Suspense for non-critical data** — render the product immediately and stream recommendations or reviews with `defer()`318- **Keep cart state server-side via cookies** — Hydrogen's cart utilities store the cart ID in a signed cookie; avoid client-only cart state that breaks SSR319- **Use the Storefront API's `@inContext` directive** — pass `language` and `country` context to get localized prices and translated content per request320- **Pin the Storefront API version in `.env`** — Shopify deprecates old API versions; explicit pinning prevents surprise breakage on API updates321322## Common Pitfalls323324| Problem | Solution |325|---------|----------|326| "Storefront API token not authorized" | Ensure the token has `unauthenticated_read_*` scopes; private tokens are only for server-side requests |327| Cart state lost between page navigations | Store cart ID in the session cookie using `cart.setCartId()`; never store cart ID in component state |328| Images not optimized on Oxygen | Use Hydrogen's `<Image>` component or the `getImageData()` helper to append Shopify CDN transform params |329| TypeScript errors on GraphQL queries | Run `npm run codegen` to regenerate types after changing queries; queries must be tagged `as const` |330| Deployment fails with "missing environment variables" | Oxygen env vars must be added in the Shopify admin under the Hydrogen deployment settings, not just in `.env` |331332## Related Skills333334- @saleor-development335- @jamstack-storefront336- @composable-commerce337- @pwa-storefront338- @commerce-api-gateway