Shopify Hydrogen vs Medusa vs Commerce.js: Headless Commerce 2026
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
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.