Medusa is an open-source headless commerce platform — @medusajs/js-sdk provides the storefront client. sdk.store.product.list({ limit: 12 }) fetches products. sdk.store.cart.create({}) creates a cart; sdk.store.cart.addLineItem(cartId, { variant_id, quantity }) adds items. sdk.store.paymentCollection.initiatePaymentSession(collectionId, { provider_id: "pp_stripe_stripe" }) starts Stripe payment. sdk.store.customer.register({ email, password, first_name }) creates accounts. sdk.store.order.list() fetches customer orders. Server-side: Medusa modules extend functionality with custom data models, event subscribers, and workflows. Modules.PRODUCT, Modules.ORDER, and Modules.CART are built-in. IOrderModuleService.createOrders() programmatically creates orders. The Admin REST API manages products, inventory, and fulfillments. Medusa runs on Node.js with PostgreSQL. Claude Code generates Medusa storefront clients, custom checkout flows, product catalog pages, and order management.
CLAUDE.md for Medusa
## Medusa Stack
- Version: @medusajs/js-sdk >= 2.6, @medusajs/medusa >= 2.6
- Client: const sdk = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, publishableKey: NEXT_PUBLIC_MEDUSA_KEY })
- Products: const { products } = await sdk.store.product.list({ limit: 12, offset: 0 })
- Cart: const { cart } = await sdk.store.cart.create({}); const cartWithItem = await sdk.store.cart.addLineItem(cart.id, { variant_id, quantity })
- Customer: sdk.store.customer.register({ email, password, first_name, last_name })
- Auth: await sdk.store.customer.login({ email, password }) — sets cookie
- Order: await sdk.store.order.confirm(cartId)
SDK Client Setup
// lib/medusa/client.ts — typed Medusa storefront client
import Medusa from "@medusajs/js-sdk"
export const medusa = new Medusa({
baseUrl: process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!,
publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY!,
debug: process.env.NODE_ENV === "development",
// Auth token stored in cookie by default in browser
auth: {
type: "session",
},
})
// Server-side admin client (never expose this token client-side)
export const medusaAdmin = new Medusa({
baseUrl: process.env.MEDUSA_BACKEND_URL!,
auth: {
type: "api-key",
apiKey: process.env.MEDUSA_API_KEY!,
},
})
Product Listing and Detail
// lib/medusa/products.ts — product queries
import { medusa } from "./client"
import type { StoreProduct, StoreProductVariant } from "@medusajs/types"
export type ProductListParams = {
limit?: number
offset?: number
categoryId?: string
collectionId?: string
tags?: string[]
search?: string
regionId?: string
}
export async function listProducts(params: ProductListParams = {}) {
const {
limit = 12,
offset = 0,
categoryId,
collectionId,
tags,
search,
regionId,
} = params
const { products, count } = await medusa.store.product.list({
limit,
offset,
category_id: categoryId ? [categoryId] : undefined,
collection_id: collectionId ? [collectionId] : undefined,
tags,
q: search,
region_id: regionId,
fields: "+variants,+variants.calculated_price",
})
return {
products: products as StoreProduct[],
total: count ?? 0,
limit,
offset,
}
}
export async function getProduct(handle: string, regionId?: string): Promise<StoreProduct | null> {
const { products } = await medusa.store.product.list({
handle,
region_id: regionId,
fields: "+variants,+variants.calculated_price,+images,+categories",
})
return products[0] as StoreProduct ?? null
}
export function getVariantPrice(variant: StoreProductVariant): string {
const price = (variant as any).calculated_price
if (!price) return "Price unavailable"
const amount = price.calculated_amount
const currency = price.currency_code?.toUpperCase() ?? "USD"
return new Intl.NumberFormat("en-US", {
style: "currency",
currency,
minimumFractionDigits: 0,
}).format(amount / 100)
}
Cart Management
// lib/medusa/cart.ts — cart state management
import { medusa } from "./client"
import type { StoreCart } from "@medusajs/types"
const CART_ID_KEY = "medusa_cart_id"
// Get or create a cart
export async function getOrCreateCart(regionId: string, email?: string): Promise<StoreCart> {
const storedId = typeof window !== "undefined"
? localStorage.getItem(CART_ID_KEY)
: null
if (storedId) {
try {
const { cart } = await medusa.store.cart.retrieve(storedId, {
fields: "+items,+items.variant,+items.variant.product,+payment_collection,+shipping_address",
})
return cart as StoreCart
} catch {
// Cart expired or not found — create new one
localStorage.removeItem(CART_ID_KEY)
}
}
const { cart } = await medusa.store.cart.create({
region_id: regionId,
email,
})
if (typeof window !== "undefined") {
localStorage.setItem(CART_ID_KEY, cart.id)
}
return cart as StoreCart
}
export async function addToCart(
cartId: string,
variantId: string,
quantity = 1,
): Promise<StoreCart> {
const { cart } = await medusa.store.cart.addLineItem(cartId, {
variant_id: variantId,
quantity,
})
return cart as StoreCart
}
export async function updateCartItem(
cartId: string,
lineItemId: string,
quantity: number,
): Promise<StoreCart> {
if (quantity === 0) {
const { cart } = await medusa.store.cart.deleteLineItem(cartId, lineItemId)
return cart as StoreCart
}
const { cart } = await medusa.store.cart.updateLineItem(cartId, lineItemId, { quantity })
return cart as StoreCart
}
export async function applyDiscount(cartId: string, code: string): Promise<StoreCart> {
const { cart } = await medusa.store.cart.update(cartId, {
promo_codes: [code],
})
return cart as StoreCart
}
export async function addShippingAddress(cartId: string, address: {
first_name: string
last_name: string
address_1: string
city: string
country_code: string
postal_code: string
phone?: string
}): Promise<StoreCart> {
const { cart } = await medusa.store.cart.update(cartId, {
shipping_address: address,
email: (address as any).email,
})
return cart as StoreCart
}
Checkout Flow
// lib/medusa/checkout.ts — Stripe checkout with Medusa
import { medusa } from "./client"
export async function initiateStripePayment(cartId: string): Promise<{
clientSecret: string
paymentCollectionId: string
}> {
// Create payment collection for the cart
const { payment_collection } = await medusa.store.paymentCollection.createForCart(cartId)
// Initialize Stripe payment session
const { payment_collection: updatedCollection } = await medusa.store.paymentCollection.initiatePaymentSession(
payment_collection.id,
{ provider_id: "pp_stripe_stripe" },
)
const stripeSession = updatedCollection.payment_sessions?.find(
s => s.provider_id === "pp_stripe_stripe",
)
if (!stripeSession?.data?.client_secret) {
throw new Error("Failed to initialize Stripe payment")
}
return {
clientSecret: stripeSession.data.client_secret as string,
paymentCollectionId: payment_collection.id,
}
}
export async function confirmOrder(cartId: string): Promise<string> {
const { order } = await medusa.store.order.confirm(cartId)
// Clear cart from localStorage
if (typeof window !== "undefined") {
localStorage.removeItem("medusa_cart_id")
}
return order.id
}
Next.js Product Page
// app/products/[handle]/page.tsx — Medusa product with add-to-cart
import { getProduct, getVariantPrice } from "@/lib/medusa/products"
import { notFound } from "next/navigation"
import type { Metadata } from "next"
import type { StoreProductVariant } from "@medusajs/types"
export async function generateMetadata({ params }: { params: { handle: string } }): Promise<Metadata> {
const product = await getProduct(params.handle)
return {
title: product?.title ?? "Product",
description: product?.description ?? undefined,
}
}
export default async function ProductPage({ params }: { params: { handle: string } }) {
const product = await getProduct(params.handle, process.env.MEDUSA_DEFAULT_REGION_ID)
if (!product) notFound()
return (
<div className="max-w-6xl mx-auto px-4 py-12">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Images */}
<div className="space-y-3">
{product.images?.map((img, i) => (
<img
key={img.id}
src={img.url}
alt={product.title}
className="w-full rounded-2xl object-cover aspect-square"
/>
))}
</div>
{/* Details */}
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold">{product.title}</h1>
{product.subtitle && (
<p className="text-muted-foreground mt-1">{product.subtitle}</p>
)}
</div>
<p className="text-muted-foreground">{product.description}</p>
{/* Variants */}
{product.variants && product.variants.length > 0 && (
<div className="space-y-3">
{product.variants.map((variant: StoreProductVariant) => (
<div key={variant.id} className="flex items-center justify-between p-3 rounded-xl border">
<span className="font-medium">{variant.title}</span>
<span className="font-bold">{getVariantPrice(variant)}</span>
</div>
))}
</div>
)}
</div>
</div>
</div>
)
}
export const revalidate = 300 // 5 minute ISR
For the Shopify Hydrogen alternative when a Shopify-powered storefront with existing products, fulfillment, and the Shopify ecosystem (apps, themes, payments) is needed — Hydrogen provides a React framework for Shopify while Medusa is database-agnostic and self-hosted, see the Shopify Hydrogen guide. For the WooCommerce/WordPress alternative when a large existing WordPress + WooCommerce store needs a headless frontend — WooCommerce’s REST API can power React storefronts, though Medusa’s code-first TypeScript API is more developer-friendly for greenfield projects. The Claude Skills 360 bundle includes Medusa skill sets covering storefront clients, cart management, and checkout flows. Start with the free tier to try e-commerce backend generation.