
Shopify Hydrogen
- 66 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Build a custom Shopify storefront with the Hydrogen React framework and Remix routing, deployed to Shopify's Oxygen edge hosting.
About
Builds headless Shopify storefronts with the Hydrogen React framework and Remix routing, deployed on Oxygen edge hosting. A developer uses it for a custom-coded Shopify frontend.
- Hydrogen React framework with Remix routing
- Deploys to Shopify Oxygen edge hosting
Shopify Hydrogen by the numbers
- 66 all-time installs (skills.sh)
- Ranked #1,172 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill shopify-hydrogenAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 66 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Build a custom Shopify storefront with the Hydrogen React framework and Remix routing, deployed to Shopify's Oxygen edge hosting.
Files
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
1. 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:3000The 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 point2. 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]),
});3. 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;4. 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>
);
}5. 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,
}),
});6. Deploy to Oxygen
npm install -g @shopify/cli
shopify hydrogen deploy
# Creates a deployment in your Shopify admin under Online Store → Themes → HeadlessFor 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 constfragment 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
languageandcountrycontext 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
{
"context": "Tests whether the agent correctly implements Hydrogen's cart system using CartForm, CartForm.getFormInput, CartForm.ACTIONS enum, and server-side cookie-based cart ID storage rather than client-side state.",
"type": "weighted_checklist",
"checklist": [
{
"name": "CartForm import from hydrogen",
"max_score": 8,
"description": "Imports `CartForm` from '@shopify/hydrogen' (not from a custom implementation or other package)"
},
{
"name": "ActionFunctionArgs from remix-oxygen",
"max_score": 7,
"description": "Imports `ActionFunctionArgs` from '@shopify/remix-oxygen' in the cart route action function"
},
{
"name": "CartForm.getFormInput usage",
"max_score": 12,
"description": "Uses `CartForm.getFormInput(formData)` to extract `action` and `inputs` from the form data in the action function"
},
{
"name": "LinesAdd action handled",
"max_score": 8,
"description": "Handles `CartForm.ACTIONS.LinesAdd` case calling `cart.addLines(inputs.lines)`"
},
{
"name": "LinesUpdate action handled",
"max_score": 8,
"description": "Handles `CartForm.ACTIONS.LinesUpdate` case calling `cart.updateLines(inputs.lines)`"
},
{
"name": "LinesRemove action handled",
"max_score": 8,
"description": "Handles `CartForm.ACTIONS.LinesRemove` case calling `cart.removeLines(inputs.lineIds)`"
},
{
"name": "cart.setCartId in response headers",
"max_score": 15,
"description": "Calls `cart.setCartId(result.cart.id)` and includes the returned headers in the JSON response"
},
{
"name": "No client-side cart state",
"max_score": 10,
"description": "Does NOT store cart ID or cart data in React state (useState), localStorage, or sessionStorage — cart state is managed server-side only"
},
{
"name": "CartForm component in AddToCartButton",
"max_score": 12,
"description": "AddToCartButton component uses the `<CartForm>` component with `route='/cart'` and `action={CartForm.ACTIONS.LinesAdd}`"
},
{
"name": "Correct lines input structure",
"max_score": 12,
"description": "The CartForm inputs for adding lines uses `{ lines: [{ merchandiseId: variantId, quantity: 1 }] }` structure"
}
]
}
Build a Complete Shopping Cart System
Problem/Feature Description
An outdoor gear retailer is building a headless Shopify storefront using Hydrogen and needs a fully functional cart experience. Customers should be able to add products to the cart from product pages, update item quantities from the cart view, and remove items they no longer want. The team has already set up the Hydrogen project and Storefront API credentials, but the cart functionality hasn't been implemented yet.
A critical requirement is that the cart must work correctly across page navigations and even if a customer opens multiple tabs. The team had a bad experience with their previous storefront where cart state was stored in JavaScript memory and was lost on refresh — this new implementation must store cart state reliably on the server side so it persists across the session.
Output Specification
Produce the following files:
1. app/routes/cart.tsx — The main cart route containing:
- A Remix
actionfunction that handles adding, updating, and removing cart lines - A React component that renders the current cart contents
2. app/components/AddToCartButton.tsx — A reusable component that renders a button to add a specific product variant to the cart, accepting a variantId prop.
3. CART_NOTES.md — A brief explanation of how cart state is persisted across requests and why this approach is used.
The files should be complete TypeScript and importable into a running Hydrogen project. Use realistic product variant IDs as placeholder values where needed (e.g., gid://shopify/ProductVariant/12345).
{
"context": "Tests whether the agent correctly configures a Hydrogen project including proper .env variable names, createHydrogenContext usage in server.ts, Oxygen deployment via GitHub Actions, Remix defer+Suspense for streaming, @inContext directive for localization, and pinned API version.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Correct .env variable names",
"max_score": 8,
"description": "The .env.example file contains all four required variables: SESSION_SECRET, PUBLIC_STOREFRONT_API_TOKEN, PUBLIC_STORE_DOMAIN, and PUBLIC_STOREFRONT_API_VERSION"
},
{
"name": "API version pinned",
"max_score": 7,
"description": "PUBLIC_STOREFRONT_API_VERSION is set to a specific dated version (e.g. '2025-01') in .env.example, not a dynamic or 'latest' value"
},
{
"name": "createHydrogenContext import",
"max_score": 8,
"description": "server.ts imports `createHydrogenContext` from '@shopify/hydrogen'"
},
{
"name": "createHydrogenContext storefront config",
"max_score": 10,
"description": "server.ts calls `createHydrogenContext` with a `storefront` object containing apiVersion, publicStorefrontToken (or privateStorefrontToken), and storeDomain fields sourced from env variables"
},
{
"name": "Session initialization in context",
"max_score": 7,
"description": "server.ts includes session configuration (e.g. `HydrogenSession.init(request, [env.SESSION_SECRET])`) within the createHydrogenContext call"
},
{
"name": "CacheLong for collections",
"max_score": 8,
"description": "The homepage loader applies `storefront.CacheLong()` to the featured collections query"
},
{
"name": "Remix defer for non-critical data",
"max_score": 10,
"description": "The homepage loader uses `defer()` (not `json()`) to return at least one non-critical data promise for streaming, and the component uses `<Suspense>` or `<Await>` to render it"
},
{
"name": "@inContext directive in query",
"max_score": 10,
"description": "At least one GraphQL query in the homepage route uses the `@inContext` directive with `language` and/or `country` parameters"
},
{
"name": "Oxygen GitHub Action",
"max_score": 10,
"description": "The oxygen.yml workflow uses `Shopify/hydrogen-action@v1` (not a generic deploy action or custom script)"
},
{
"name": "GitHub Action secrets reference",
"max_score": 7,
"description": "The oxygen.yml workflow references secrets for shop domain and CLI token (e.g. `${{ secrets.SHOPIFY_SHOP_DOMAIN }}` and `${{ secrets.SHOPIFY_CLI_TOKEN }}`)"
},
{
"name": "Oxygen env vars beyond .env",
"max_score": 15,
"description": "SETUP_NOTES.md or equivalent documentation mentions that environment variables must be configured in Shopify admin (not only in .env) for Oxygen production deployments"
}
]
}
Configure a Hydrogen Storefront and Set Up Deployment
Problem/Feature Description
A home goods brand has hired a development agency to build their new headless Shopify storefront. The agency needs to set up the complete project from scratch: scaffold the app, wire up the Storefront API, build a homepage that showcases featured collections, and prepare the project for production deployment on Shopify's edge infrastructure. The brand operates in multiple markets (US, UK, AU) and wants to make sure the architecture supports international pricing from the start.
The homepage should load featured collections quickly — performance is a top priority since the brand has measured that every 100ms of load time costs them conversion rate. At the same time, personalized "Recently Viewed" product recommendations for logged-in customers should load without blocking the main page content. The deployment pipeline should use GitHub Actions so that every push to main automatically deploys to production.
Output Specification
Produce the following files representing key parts of the project setup:
1. .env.example — Template environment file with all required Storefront API configuration variables (with placeholder values, not real credentials).
2. server.ts — The Hydrogen entry point file that initializes the Hydrogen context using the environment variables, including both public and private storefront tokens and session configuration.
3. app/routes/_index.tsx — Homepage loader and component that fetches featured collections. The loader should optimize for fast initial page load while ensuring personalized "Recently Viewed" recommendations don't block rendering. Include a GraphQL query that requests localized data for international markets.
4. .github/workflows/oxygen.yml — GitHub Actions workflow file for deploying to Oxygen on push to main.
5. SETUP_NOTES.md — Notes explaining the caching strategy decisions, how environment variables are managed across different environments (local development and production), and how API version compatibility is maintained over time.
{
"context": "Tests whether the agent correctly uses Hydrogen's storefront.query with appropriate caching strategies, proper import paths, TypeScript-tagged GraphQL queries, Hydrogen-specific components, and localization directives when building Hydrogen route files.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Correct import for json/LoaderFunctionArgs",
"max_score": 10,
"description": "Imports `json` and/or `LoaderFunctionArgs` from '@shopify/remix-oxygen' (not from '@remix-run/node' or '@remix-run/server-runtime')"
},
{
"name": "Correct import for useLoaderData",
"max_score": 5,
"description": "Imports `useLoaderData` from '@remix-run/react'"
},
{
"name": "storefront.query usage",
"max_score": 10,
"description": "Uses `storefront.query(QUERY, { variables, cache })` to fetch data from the Storefront API (not a raw fetch or GraphQL client)"
},
{
"name": "CacheLong for catalog data",
"max_score": 10,
"description": "Applies `storefront.CacheLong()` to at least one query for catalog/collection data (product lists or collection metadata)"
},
{
"name": "CacheShort for inventory data",
"max_score": 10,
"description": "Applies `storefront.CacheShort()` to a query involving product availability or inventory-sensitive fields"
},
{
"name": "CacheNone for personalized data",
"max_score": 5,
"description": "Uses `storefront.CacheNone()` for any customer-specific or personalized query (if present)"
},
{
"name": "GraphQL queries tagged as const",
"max_score": 10,
"description": "All GraphQL query template literals are suffixed with `as const`"
},
{
"name": "Queries colocated with routes",
"max_score": 5,
"description": "GraphQL query strings are defined in the same file as their loader (not imported from a separate queries file)"
},
{
"name": "404 handling via Response throw",
"max_score": 5,
"description": "Throws `new Response('Not Found', { status: 404 })` (or equivalent) when a product/collection is not found, rather than returning null"
},
{
"name": "Hydrogen Image component",
"max_score": 10,
"description": "Uses Hydrogen's `<Image>` component (from '@shopify/hydrogen') for rendering product or collection images, not a plain `<img>` tag"
},
{
"name": "Hydrogen Money component",
"max_score": 10,
"description": "Uses Hydrogen's `<Money>` component (from '@shopify/hydrogen') for rendering prices, not manual string formatting"
},
{
"name": "@inContext directive for localization",
"max_score": 10,
"description": "At least one GraphQL query uses the `@inContext` directive with `language` and/or `country` variables for localized prices or content"
}
]
}
Build Hydrogen Storefront Route Files
Problem/Feature Description
A fashion brand is migrating from a legacy Shopify theme to a custom headless storefront built with Hydrogen. The engineering team has scaffolded the project and connected it to the Storefront API, but the core product browsing experience still needs to be built out. They need two key route files: one for a product detail page and one for a collections listing page.
The team's primary concern is performance — their old storefront had slow page loads and poor TTFB scores. The new implementation needs to be smart about caching: product catalog data should stay cached aggressively at the CDN edge, inventory-sensitive data (like availability) should refresh frequently, and any customer-specific queries should never be cached. The brand also sells internationally and the team wants to ensure prices are fetched in the visitor's local currency and language.
Output Specification
Produce two TypeScript route files that would slot into a Hydrogen + Remix project:
1. app/routes/products.$handle.tsx — Product detail page loader and component. Should fetch a product by its handle, including variants with pricing and availability. Must handle the case where a product doesn't exist.
2. app/routes/collections.$handle.tsx — Collection listing page loader and component. Should fetch a collection and its products, supporting sort order via URL query parameters.
For each file, include the loader function, any GraphQL queries, and a basic React component that renders the data using useLoaderData. The files should be complete and runnable in a Hydrogen project.
Also produce a NOTES.md file documenting the caching strategy decisions made for each query and why.
{
"name": "finsi/shopify-hydrogen",
"version": "0.1.0",
"summary": "Hydrogen + Remix storefront with Oxygen deployment and Storefront API",
"skills": {
"shopify-hydrogen": {
"path": "SKILL.md"
}
}
}