
If you’ve heard that Shopify “runs on Remix,” you’re not wrong: Hydrogen (Shopify’s JavaScript framework for custom storefronts) is built on top of Remix concepts—loaders, actions, nested routes, and server rendering—and Oxygen is Shopify’s edge hosting for it.
This post gives you the big picture, trade-offs, and a starter blueprint.
What is Remix (in one minute)?
Remix is a full-stack web framework focused on web standards, progressive enhancement, and server-side rendering by default. You structure your app with nested routes. Each route can export:
- a
loader()
for server data fetching, - an
action()
for form mutations, - and a default React component that renders UI.
Because loaders run on the server, Remix encourages fast, cacheable responses and keeps secrets off the client. It also embraces forms and HTTP semantics—things the browser already knows how to do—so you ship less JS and get more reliability.
Why Shopify uses Remix (via Hydrogen)
Shopify’s Hydrogen framework adopts Remix’s routing and data primitives and adds commerce utilities:
- Storefront API helpers (GraphQL) for products, collections, carts, and customer data.
- Server rendering with streaming for fast Time-to-First-Byte on product pages.
- Oxygen hosting (global edge runtime) for low latency worldwide.
- Built-in patterns for SEO, image optimization, and i18n.
Hydrogen doesn’t replace Liquid themes (Online Store 2.0); it’s a separate path for teams that need a fully custom, headless storefront.
Pros & Cons of Remix for Shopify
Pros
- Performance by default: SSR + route-level data loading; easy edge caching.
- Great dev ergonomics: nested routes keep data close to UI; fewer client fetch effects.
- Progressive enhancement: works without JS; enhances with JS automatically.
- Predictable mutations:
action()
+<Form>
= resilient add-to-cart, login, etc. - First-class with Hydrogen: batteries included for Storefront API, carts, images, SEO.
- Edge-ready: Deploy to Oxygen or any platform that supports Remix adapters.
Cons
- Two stacks to learn if you’re coming from Liquid only (Remix + Shopify APIs).
- Infrastructure shift: you own a custom app front end—monitoring, logs, CI/CD.
- Theme apps vs. headless: some apps are theme-centric; you’ll use app proxies or direct APIs.
- Content modeling: without sections/blocks from Liquid, you’ll likely add a CMS (e.g., Sanity, Contentful) or use Metaobjects/Metafields.
When to choose Liquid vs. Remix (Hydrogen)
Choose Liquid (Online Store 2.0) if:
- You want fast time-to-launch, theme customizations, and App Store compatibility.
- Merchants or marketers need Theme Editor control (sections, blocks, presets).
Choose Remix/Hydrogen if:
- You need fully custom UX or complex flows (bundling, subscriptions, configurators).
- You want edge SSR with granular caching and sub-100ms TTFB.
- Your team prefers React server patterns and GraphQL.
Many brands run Liquid for core + Hydrogen microsites (campaigns, product labs) to balance speed to market with performance.
Architecture snapshot
- Frontend: Remix (Hydrogen) routes/components.
- Data: Shopify Storefront API (GraphQL) for catalog/cart, optional CMS for content.
- Auth: Customer accounts via Storefront API; admin ops via Admin API (server only).
- Edge: Oxygen (global) or another Remix host.
- Payments/Checkout: Shopify Checkout via cart/checkout APIs and redirect.
Minimal product page: loader + component
// app/routes/products.$handle/route.tsx
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
const STOREFRONT_API_URL = `https://${process.env.SHOPIFY_STORE_DOMAIN}/api/2024-10/graphql.json`;
const STOREFRONT_TOKEN = process.env.SHOPIFY_STOREFRONT_TOKEN!;
export async function loader({ params }: LoaderFunctionArgs) {
const handle = params.handle!;
const query = `
query ProductByHandle($handle: String!) {
product(handle: $handle) {
id
title
description
handle
featuredImage { url altText width height }
variants(first: 10) {
nodes { id title price { amount currencyCode } availableForSale }
}
}
}
`;
const res = await fetch(STOREFRONT_API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Shopify-Storefront-Access-Token": STOREFRONT_TOKEN
},
body: JSON.stringify({ query, variables: { handle } })
});
const { data } = await res.json();
if (!data?.product) throw new Response("Not Found", { status: 404 });
return json({ product: data.product }, { headers: { "Cache-Control": "public, max-age=30, s-maxage=300, stale-while-revalidate=86400" } });
}
export default function ProductRoute() {
const { product } = useLoaderData<typeof loader>();
return (
<main className="mx-auto max-w-4xl p-6">
<h1 className="text-3xl font-bold">{product.title}</h1>
{product.featuredImage && (
<img
src={product.featuredImage.url}
alt={product.featuredImage.altText ?? product.title}
width={product.featuredImage.width}
height={product.featuredImage.height}
loading="eager"
fetchPriority="high"
className="mt-4 w-full rounded-xl"
/>
)}
<p className="mt-4 opacity-80">{product.description}</p>
<form method="post" className="mt-6">
<input type="hidden" name="variantId" value={product.variants.nodes[0]?.id} />
<button type="submit" className="rounded-xl px-5 py-3 font-medium border">
Add to cart
</button>
</form>
</main>
);
}
Add-to-cart action
// app/routes/products.$handle/route.tsx (continued)
import type { ActionFunctionArgs } from "@remix-run/node";
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const variantId = String(formData.get("variantId"));
const mutation = `
mutation CartCreate($lines: [CartLineInput!]!) {
cartCreate(input: { lines: $lines }) {
cart { id checkoutUrl }
userErrors { message }
}
}
`;
const r = await fetch(STOREFRONT_API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Shopify-Storefront-Access-Token": STOREFRONT_TOKEN
},
body: JSON.stringify({ query: mutation, variables: { lines: [{ merchandiseId: variantId, quantity: 1 }] } })
}).then(r => r.json());
const checkoutUrl = r?.data?.cartCreate?.cart?.checkoutUrl;
if (checkoutUrl) return new Response(null, { status: 303, headers: { Location: checkoutUrl } });
return new Response("Cart error", { status: 400 });
}
TL;DR
- Liquid: fastest for theme-driven stores and app compatibility.
- Remix/Hydrogen: best for custom UX + edge performance, with more engineering ownership.
- Start small, ship a Hydrogen microsite, and expand as you validate results.