Skip to main content

Shopify Hydrogen vs Medusa vs Commerce.js

·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

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

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.