Skip to main content

Guide

Shopify Hydrogen vs Medusa vs Commerce.js 2026

Shopify Hydrogen vs Medusa.js vs Commerce.js compared for headless e-commerce. Cart, checkout, product catalog, React/Next.js integration, and self-hosted vs.

·PkgPulse Team·
0

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 SDKuseCart(), 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

FeatureShopify HydrogenMedusaCommerce.js
ArchitectureShopify-managed backendSelf-hosted Node.jsSaaS API
Open-source❌ (framework only)✅ MIT
Self-hostable❌ (Shopify required)
Per-transaction feesShopify plan + 2%
Payment processorsShopify Payments (+ others)Stripe, PayPal, moreStripe, 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 appsGrowingBasic
GitHub stars4.1k26k2.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

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.