Shopify Hydrogen vs Medusa vs Commerce.js: Headless Commerce 2026
TL;DR
Headless commerce separates the frontend (your React app) from the backend (product catalog, checkout, payments). The options split by architecture: Shopify Hydrogen is the official Shopify Remix framework — connects to Shopify's managed backend (products, checkout, payments, inventory) through the Storefront API; you get Shopify's ecosystem (payment processors, app store, fulfillment) with a custom React frontend. Medusa is the open-source commerce backend — a Node.js server you deploy yourself, with a REST API and modules for products, carts, orders, payments, and fulfillment; pair it with any frontend framework, fully customizable, no per-transaction fees. Commerce.js is the SaaS headless commerce API — managed backend with a developer-friendly SDK, products, cart, and checkout without self-hosting; designed for developers who want to integrate commerce into an existing site without running an e-commerce server. For Shopify merchants going headless: Hydrogen. For full-control custom commerce backends: Medusa. For API-first commerce in an existing app: Commerce.js.
Key Takeaways
- Shopify Hydrogen uses Remix — file-based routing, server components, and Shopify Storefront API
- Medusa is open-source — MIT license, self-hosted, no per-transaction fees
- Commerce.js charges per API call — $0/month for 50 products and basic features
- Hydrogen requires a Shopify plan — backend is Shopify (Basic: $39/month+)
- Medusa has a module system — swap payment providers (Stripe, PayPal), fulfillment, CMS
- Commerce.js has a React SDK —
useCart(),useCheckout()hooks out of the box - All three support Next.js — via Storefront API, REST API, or SDK
Architecture Comparison
Shopify Hydrogen:
Remix (your frontend) → Shopify Storefront API → Shopify backend (managed)
✅ Shopify payments, checkout, inventory, apps
❌ Can't customize backend logic; Shopify fees apply
Medusa:
Any frontend → Medusa REST API → Medusa Node.js server → PostgreSQL + Redis
✅ Full customization, self-host, no platform fees
❌ You manage server infrastructure
Commerce.js:
Any frontend → Commerce.js SDK → Commerce.js managed API
✅ No server management, developer-friendly SDK
❌ SaaS pricing at scale; limited customization
Shopify Hydrogen: Official Headless Shopify
Hydrogen is Shopify's Remix-based React framework for building headless storefronts. It connects to any Shopify store via the Storefront API.
Setup
npm create @shopify/hydrogen@latest
# Choose a template: Hello World, Demo Store
cd my-store
npm install
npm run dev
Storefront API Client
// lib/shopify.ts
import { createStorefrontClient } from "@shopify/hydrogen";
export const client = createStorefrontClient({
storeDomain: process.env.PUBLIC_STORE_DOMAIN!, // "mystore.myshopify.com"
publicStorefrontToken: process.env.PUBLIC_STOREFRONT_API_TOKEN!,
privateStorefrontToken: process.env.PRIVATE_STOREFRONT_API_TOKEN,
storefrontVersion: "2024-01",
});
// Query type helper
export const { storefront } = client;
Fetch Products
// app/routes/products.tsx (Remix route)
import { json, type LoaderFunctionArgs } from "@shopify/remix-oxygen";
import { useLoaderData } from "@remix-run/react";
const PRODUCTS_QUERY = `#graphql
query Products($first: Int!, $cursor: String) {
products(first: $first, after: $cursor, sortKey: BEST_SELLING) {
nodes {
id
title
handle
featuredImage {
url
altText
width
height
}
priceRange {
minVariantPrice {
amount
currencyCode
}
}
variants(first: 5) {
nodes {
id
title
availableForSale
price {
amount
currencyCode
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
export async function loader({ context }: LoaderFunctionArgs) {
const { storefront } = context;
const { products } = await storefront.query(PRODUCTS_QUERY, {
variables: { first: 24 },
});
return json({ products });
}
export default function ProductsPage() {
const { products } = useLoaderData<typeof loader>();
return (
<div className="products-grid">
{products.nodes.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
Cart Management
// Cart operations via Storefront API
const ADD_TO_CART_MUTATION = `#graphql
mutation AddToCart($cartId: ID!, $lines: [CartLineInput!]!) {
cartLinesAdd(cartId: $cartId, lines: $lines) {
cart {
id
totalQuantity
lines(first: 100) {
nodes {
id
quantity
merchandise {
... on ProductVariant {
id
title
price { amount currencyCode }
product { title }
}
}
}
}
cost {
totalAmount { amount currencyCode }
}
}
}
}
`;
const CREATE_CART_MUTATION = `#graphql
mutation CreateCart($lines: [CartLineInput!]) {
cartCreate(input: { lines: $lines }) {
cart {
id
checkoutUrl
}
}
}
`;
// Cart action in Remix
export async function action({ request, context }: ActionFunctionArgs) {
const { storefront } = context;
const formData = await request.formData();
const variantId = formData.get("variantId") as string;
const cartId = formData.get("cartId") as string | null;
if (!cartId) {
// Create new cart
const { cartCreate } = await storefront.mutate(CREATE_CART_MUTATION, {
variables: { lines: [{ merchandiseId: variantId, quantity: 1 }] },
});
return json({ cart: cartCreate.cart });
}
// Add to existing cart
const { cartLinesAdd } = await storefront.mutate(ADD_TO_CART_MUTATION, {
variables: {
cartId,
lines: [{ merchandiseId: variantId, quantity: 1 }],
},
});
return json({ cart: cartLinesAdd.cart });
}
Medusa: Open-Source Commerce Backend
Medusa is a Node.js commerce platform you self-host — full control over business logic, payment processors, fulfillment providers, and data.
Installation
npx create-medusa-app@latest my-store
# Installs: Medusa backend + Next.js storefront template
cd my-store/backend
npm run dev # Runs on :9000
Product Management (REST API)
// Fetch products from your Medusa backend
async function getProducts() {
const response = await fetch(
`${process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL}/store/products`,
{
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY!,
},
}
);
const { products } = await response.json();
return products;
}
// Fetch a single product by handle
async function getProduct(handle: string) {
const response = await fetch(
`${process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL}/store/products?handle=${handle}`,
{
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY!,
},
}
);
const { products } = await response.json();
return products[0];
}
Cart and Checkout Flow
import Medusa from "@medusajs/medusa-js";
const medusa = new Medusa({
baseUrl: process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!,
maxRetries: 3,
publishableApiKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY!,
});
// Create cart
async function createCart(regionId: string) {
const { cart } = await medusa.carts.create({ region_id: regionId });
localStorage.setItem("cart_id", cart.id);
return cart;
}
// Add item to cart
async function addToCart(variantId: string, quantity: number = 1) {
let cartId = localStorage.getItem("cart_id");
if (!cartId) {
const cart = await createCart("reg_XXXXXXXX");
cartId = cart.id;
}
const { cart } = await medusa.carts.lineItems.create(cartId, {
variant_id: variantId,
quantity,
});
return cart;
}
// Complete checkout
async function checkout(cartId: string, email: string, address: Address) {
// 1. Add customer email
await medusa.carts.update(cartId, { email });
// 2. Add shipping address
await medusa.carts.update(cartId, { shipping_address: address });
// 3. List available shipping options
const { shipping_options } = await medusa.shippingOptions.listCartOptions(cartId);
// 4. Select shipping method
await medusa.carts.addShippingMethod(cartId, {
option_id: shipping_options[0].id,
});
// 5. Get Stripe payment session
await medusa.carts.createPaymentSessions(cartId);
const { cart } = await medusa.carts.setPaymentSession(cartId, {
provider_id: "stripe",
});
return cart;
}
Custom Medusa Module (Extending Backend)
// Custom loyalty points module — src/modules/loyalty/index.ts
import { Module } from "@medusajs/utils";
import LoyaltyModuleService from "./service";
export const LOYALTY_MODULE = "loyaltyModuleService";
export default Module(LOYALTY_MODULE, {
service: LoyaltyModuleService,
});
// Service implementation
class LoyaltyModuleService {
async getPoints(customerId: string): Promise<number> {
return await this.loyaltyRepository.getBalance(customerId);
}
async addPoints(customerId: string, orderId: string, amount: number): Promise<void> {
const points = Math.floor(amount / 100); // 1 point per dollar
await this.loyaltyRepository.addTransaction(customerId, orderId, points);
}
}
Commerce.js: SaaS Headless Commerce
Commerce.js is a managed API platform with a React SDK — add products, cart, and checkout to any existing app without managing a commerce server.
Installation
npm install @chec/commerce.js
Setup
import Commerce from "@chec/commerce.js";
export const commerce = new Commerce(process.env.NEXT_PUBLIC_CHEC_PUBLIC_KEY!, true);
Fetch Products
import { useEffect, useState } from "react";
import { commerce } from "@/lib/commerce";
import type { Product } from "@chec/commerce.js/types/product";
function useProducts() {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
commerce.products.list({ limit: 20, sortBy: "created", sortDirection: "desc" }).then(({ data }) => {
setProducts(data);
setLoading(false);
});
}, []);
return { products, loading };
}
function ProductsPage() {
const { products, loading } = useProducts();
if (loading) return <Spinner />;
return (
<div>
{products.map((product) => (
<div key={product.id}>
<img src={product.image?.url} alt={product.name} />
<h2>{product.name}</h2>
<p>{product.price.formatted_with_symbol}</p>
</div>
))}
</div>
);
}
Cart Hooks
import { useState } from "react";
import { commerce } from "@/lib/commerce";
import type { Cart } from "@chec/commerce.js/types/cart";
function useCart() {
const [cart, setCart] = useState<Cart | null>(null);
async function addToCart(productId: string, quantity: number = 1) {
const updatedCart = await commerce.cart.add(productId, quantity);
setCart(updatedCart.cart);
}
async function updateCartItem(lineItemId: string, quantity: number) {
const updatedCart = await commerce.cart.update(lineItemId, { quantity });
setCart(updatedCart.cart);
}
async function removeFromCart(lineItemId: string) {
const updatedCart = await commerce.cart.remove(lineItemId);
setCart(updatedCart.cart);
}
async function emptyCart() {
const updatedCart = await commerce.cart.empty();
setCart(updatedCart.cart);
}
return { cart, addToCart, updateCartItem, removeFromCart, emptyCart };
}
Checkout
async function generateCheckoutToken(cartId: string) {
return await commerce.checkout.generateToken(cartId, { type: "cart" });
}
async function captureOrder(checkoutTokenId: string, orderDetails: {
customer: { firstname: string; lastname: string; email: string };
shipping: { name: string; street: string; town_city: string; county_state: string; postal_zip_code: string; country: string };
payment: { gateway: string; stripe: { payment_method_id: string } };
}) {
const order = await commerce.checkout.capture(checkoutTokenId, orderDetails);
return order;
}
Feature Comparison
| Feature | Shopify Hydrogen | Medusa | Commerce.js |
|---|---|---|---|
| Architecture | Shopify-managed backend | Self-hosted Node.js | SaaS API |
| Open-source | ❌ (framework only) | ✅ MIT | ❌ |
| Self-hostable | ❌ (Shopify required) | ✅ | ❌ |
| Per-transaction fees | Shopify plan + 2% | ❌ | ❌ |
| Payment processors | Shopify Payments (+ others) | Stripe, PayPal, more | Stripe, Braintree |
| Inventory management | ✅ Shopify | ✅ | ✅ Basic |
| Multi-currency | ✅ | ✅ | ✅ |
| Multi-region | ✅ Shopify Markets | ✅ | ❌ |
| Custom business logic | ❌ | ✅ Modules | ❌ |
| React SDK | ✅ Hydrogen hooks | ✅ medusa-react | ✅ |
| App ecosystem | ✅ 8,000+ Shopify apps | Growing | Basic |
| GitHub stars | 4.1k | 26k | 2.5k |
When to Use Each
Choose Shopify Hydrogen if:
- You're a Shopify merchant who wants a custom frontend (brand control) with Shopify's backend
- Shopify's app ecosystem (reviews, loyalty, email marketing) is important
- Shopify Payments simplifies PCI compliance
- International selling via Shopify Markets (multi-currency, duties, tax)
- You want Remix server components and optimistic UI built-in
Choose Medusa if:
- Full control over backend business logic (custom pricing rules, B2B portals, bundles)
- No per-transaction fees at scale (important for high-volume merchants)
- Self-hosting on your own infrastructure (cost control, data sovereignty)
- Custom integrations not available in Shopify's app store
- Open-source community and extensible module system
Choose Commerce.js if:
- Adding commerce to an existing website or app (blog, landing page, SaaS)
- No server management — just a public API key and the SDK
- Small to medium catalog (under 1,000 products) without complex inventory
- Fastest time to a working shop — no infrastructure setup
PCI Compliance and Payment Security Architecture
Payment security is non-negotiable in e-commerce, and the compliance burden varies significantly between these platforms. Shopify Hydrogen inherits Shopify's PCI DSS Level 1 certification — the highest level — because Shopify's backend handles all card data. Your Hydrogen frontend never touches raw card numbers; customers enter payment information in Shopify's hosted checkout or Stripe Elements components that tokenize the data before it leaves the browser. Medusa's self-hosted architecture means your server is in the payment data flow unless you configure payment providers carefully. Using Stripe's Elements or PayPal's Smart Payment Buttons with Medusa means the card data is tokenized on the client and only the token reaches your Medusa server, which significantly reduces your PCI scope. However, self-hosting Medusa means you are responsible for server security, dependency patching, and audit logging — scope that Shopify handles entirely on your behalf. Commerce.js processes payments through its connected gateway integrations, similarly limiting your PCI scope. For teams without dedicated security expertise, Shopify's managed compliance posture is a meaningful risk reduction.
International Commerce and Multi-Region Architecture
Selling internationally introduces complexity in currency display, tax calculation, shipping rates, and legal compliance that varies by platform. Shopify Markets handles multi-currency storefronts natively — customers see prices in their local currency, and Shopify automatically calculates duties and taxes using its global tax database. The @shopify/hydrogen package exposes market context and currency conversion utilities that make building localized storefronts straightforward. Medusa's region system is designed for this: you configure separate regions for each country or currency zone, each with its own currency, tax rates, payment providers, and fulfillment options. The multi-region architecture requires more upfront configuration but gives you fine-grained control over regional pricing strategies — selling at a different EUR price in Germany vs. France, for example. Commerce.js has basic multi-currency support but lacks the regional tax and fulfillment configuration depth of Shopify or Medusa. For products targeting more than two or three markets, Medusa's explicit region model or Shopify Markets are better choices than Commerce.js's simpler international support.
Content Management and Headless CMS Integration
Headless e-commerce storefronts typically pair their commerce backend with a headless CMS for editorial content — blog posts, landing pages, product guides, and marketing campaigns that require non-developer content editing. All three platforms integrate with headless CMSs but require different integration patterns. Shopify Hydrogen with Remix integrates naturally with Contentful, Sanity, or Hygraph through server loaders that fetch content alongside Storefront API product data in the same route. Medusa's Next.js starter template includes integration examples for Contentful and Strapi. Commerce.js's lightweight SDK works similarly with any headless CMS fetched client-side or server-side. The key architectural decision is whether product and editorial content should be fetched in separate requests (simpler but slower) or unified in a single server-rendered payload (more complex but faster). Hydrogen's Remix loader pattern makes the unified approach clean, while Next.js RSC parallel data fetching with Promise.all() achieves similar results for Medusa and Commerce.js-based storefronts.
Self-Hosting Medusa at Production Scale
Medusa's self-hosted architecture gives full control but requires production infrastructure that many solo developers and small teams underestimate. A production Medusa deployment needs at minimum: a managed PostgreSQL instance (Neon, Railway, or Supabase for developer-friendly options), a Redis instance for queue processing and caching, a file storage service for product media (S3-compatible), and a Node.js hosting environment with enough memory to handle concurrent requests (Medusa recommends at least 512 MB RAM, 1 GB for moderate traffic). Railway's one-click Medusa template and Render's managed PostgreSQL make this easier than setting up raw EC2 instances, but you still own the security patching, backup strategy, and uptime monitoring. Medusa's v2 release introduced a modular architecture where each commerce capability (inventory, payment, fulfillment) is a separately deployable module — allowing you to scale only the components under load. For products expecting significant traffic from launch, engage a DevOps resource to design the deployment architecture before going live rather than retrofitting reliability practices after your first traffic spike.
Methodology
Data sourced from official Shopify Hydrogen documentation (shopify.dev/docs/custom-storefronts/hydrogen), Medusa documentation (docs.medusajs.com), Commerce.js documentation (commercejs.com/docs), GitHub star counts as of February 2026, npm download statistics, pricing pages as of February 2026, and community discussions from the Medusa Discord, Shopify Partners community, and r/ecommerce.
Related: Stripe Billing vs Chargebee vs Recurly for recurring subscription billing that complements these e-commerce backends, or Resend vs SendGrid vs Brevo for order confirmation and transactional emails.
See also: React vs Vue and React vs Svelte