Building a Headless Store with MedusaJS and Next.js
Author
Yousif Atabani
Date Published

Shopify takes 0.5–2% of every sale if you use a third-party payment gateway — and locks checkout customization behind a $2,000/month Plus plan. MedusaJS charges nothing, and you own every line of code. We've built headless storefronts for clients who outgrew Shopify's constraints, and this is the stack we reach for.
This guide walks you through setting up a MedusaJS backend, connecting a Next.js storefront, and building a working product catalog with cart and checkout — from zero to a functional store.
Why MedusaJS Over Shopify?
MedusaJS is an open-source headless commerce engine built on Node.js and TypeScript, released under the MIT license. No licensing fees, no transaction charges, no revenue sharing. Shopify's pricing runs $39–$399/month before apps and transaction fees — Shopify Plus starts at $2,000/month.
The cost gap compounds. A business processing $100,000/month through Shopify with third-party payment gateways can expect $40,000–$60,000 in platform costs over three years. The same operation on MedusaJS runs $15,000–$25,000 — mostly infrastructure and development. The break-even point sits around $30,000–$50,000 in monthly revenue.
MedusaJS | Shopify | |
|---|---|---|
License | MIT (free) | $39–$399/mo; Plus from $2,000/mo |
Transaction Fees | None | 0.5–2% on third-party gateways |
Checkout Customization | Full control | Requires Plus ($2,000/mo) |
Data Ownership | 100% — your database, your servers | Shopify controls infrastructure |
Payment Providers | Stripe, PayPal, Klarna, Adyen, custom | Shopify Payments preferred; penalties for others |
App Ecosystem | 100+ plugins (growing) | 8,000+ apps |
Technical Requirement | Development team needed | Non-technical friendly |
Beyond cost, the architectural difference matters. Medusa's modular design decouples commerce functions — cart, products, orders, inventory — into independent modules. You swap or extend any piece without touching the rest. Shopify's monolithic architecture means workarounds, not solutions.
What You'll Need
Before starting, make sure you have these installed:
That's it. No proprietary SDKs, no partner certifications, no platform approval process.
Set Up the Medusa Backend
One command scaffolds both the backend and the Next.js storefront:
1npx create-medusa-app@latest my-store --with-nextjs-starter
This creates two directories: my-store (the Medusa backend with admin dashboard) and my-store-storefront (the Next.js frontend). The installer also creates a PostgreSQL database automatically.
Start the development server:
1cd my-store2npm run dev
Three things are now running:
- Medusa server at http://localhost:9000
- Admin dashboard at http://localhost:9000/app
- Next.js storefront at http://localhost:8000
Create your admin user if the installer didn't prompt you:
1npx medusa user -e admin@yourstore.com -p your-secure-password
Log into the admin dashboard, configure at least one region (this sets currency and tax rules), and add a few test products. The storefront won't display anything until products exist in the system.
Connect the Next.js Storefront
The storefront communicates with Medusa through the JS SDK — an NPM package that wraps Medusa's Store API. Every request from the storefront requires a publishable API key in the header, which scopes requests to specific sales channels.
Grab your publishable API key from the admin dashboard: Settings → Publishable API Keys. Then set your environment variables in the storefront's .env.local:
1NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_your_key_here2MEDUSA_BACKEND_URL=http://localhost:9000
The SDK is already included in the starter. If you're building from scratch, install it:
1npm install @medusajs/js-sdk
Initialize it in a shared module (e.g., lib/sdk.ts) so every component imports from the same instance. The publishable key gets attached to every Store API request automatically.
Build the Product Catalog
With the SDK configured, fetching products is straightforward. Here's a product listing component:
1"use client"23import { useEffect, useState } from "react"4import { HttpTypes } from "@medusajs/types"5import { sdk } from "@/lib/sdk"67export default function Products() {8 const [loading, setLoading] = useState(true)9 const [products, setProducts] = useState<10 HttpTypes.StoreProduct[]11 >([])1213 useEffect(() => {14 if (!loading) return1516 sdk.store.product.list()17 .then(({ products: dataProducts }) => {18 setProducts(dataProducts)19 setLoading(false)20 })21 }, [loading])2223 return (24 <ul>25 {products.map((product) => (26 <li key={product.id}>27 <h3>{product.title}</h3>28 <p>{product.description}</p>29 </li>30 ))}31 </ul>32 )33}
sdk.store.product.list() returns all published products scoped to the sales channel tied to your publishable key. For product detail pages, use sdk.store.product.retrieve(productId) with Next.js dynamic routes.
In production, we'd move this to a Server Component using async/await instead of useEffect — removing the loading state entirely and letting Next.js handle data fetching at the edge. The starter storefront already follows this pattern.
Cart and Checkout
Create a cart when the user first visits the store. Store the cart ID in localStorage so it persists across page loads:
1// Create cart2sdk.store.cart.create({ region_id: region.id })3 .then(({ cart }) => {4 localStorage.setItem("cart_id", cart.id)5 })67// Add item to cart8sdk.store.cart.createLineItem(cartId, {9 variant_id: selectedVariant.id,10 quantity: 1,11})1213// Update quantity14sdk.store.cart.updateLineItem(cartId, itemId, {15 quantity: newQuantity,16})1718// Remove item19sdk.store.cart.deleteLineItem(cartId, itemId)
For a real store, wrap the cart state in a React Context so any component — header cart icon, product page add-to-cart button, cart drawer — can read and update it without prop drilling.
Medusa's checkout is a five-step process, each a single SDK call:
- Email — attach the customer's email to the cart
- Address — add shipping and billing addresses
- Shipping — fetch available methods, set one on the cart
- Payment — initialize a payment session (Stripe, PayPal, etc.)
- Complete — finalize the order, decrement inventory, fire confirmation events
The steps are customizable — you can combine address and shipping into one form, add express checkout, or rearrange the flow entirely. This is exactly the flexibility Shopify locks behind Plus.
When to Choose This Stack
Choose MedusaJS + Next.js if:
You need custom checkout flows, B2B pricing models, or subscription logic. You're processing enough revenue that Shopify's transaction fees add up ($30K+/mo). You want 100% data ownership and no platform lock-in. Your team includes developers who can manage infrastructure.
Choose Shopify if:
You need to launch in days, not weeks. You don't have a development team. Your catalog is simple and standard checkout works fine.
Headless storefronts built with Next.js consistently achieve 90+ Lighthouse performance scores, compared to Shopify themes averaging 60–75. That performance gap translates to real money: 25% higher conversion rates from faster page loads and fully custom checkout experiences.
MedusaJS gives you the commerce engine. Next.js gives you the storefront performance. For any startup past the prototype stage that needs control over checkout, pricing, or customer experience — this stack eliminates the platform tax and puts the engineering back in your hands.
References
Install Medusa — Medusa Documentation
Next.js Starter Storefront — Medusa Documentation
MedusaJS vs Shopify: Headless Commerce — Born Digital
Shopify vs. Medusa: Which One is Better? — Medusa Blog
Why MedusaJS is the Future of Headless Ecommerce — LinearLoop
Storefront Development Guides — Medusa Documentation
Headless Commerce: Next.js Storefront Dev Guide — Digital Applied