Skip to main content

Shopify Hydrogen vs Medusa vs Commerce.js: Headless Commerce 2026

·PkgPulse Team

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

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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.