Guides

Building E-Commerce with open-source tools

This guide details the implementation of a headless e-commerce architecture using Medusa as the backend commerce engine and Next.js for the storefront. It focuses on solving common pain points such as cart state synchronization, regional payment handling with Stripe, and performance optimization for high-traffic product listing pages.

4-6 hours6 steps
1

Initialize Medusa Backend and PostgreSQL Schema

Create the core commerce engine. This setup configures the database schema for products, variants, and orders. Use the --seed flag to populate initial data for testing relationships.

setup-backend.sh
npx create-medusa-app@latest --seed

⚠ Common Pitfalls

  • Ensure the PostgreSQL database exists before running the command.
  • Verify that the DATABASE_URL in the .env file includes the correct credentials and port.
2

Configure Stripe for Regional Payment Processing

Install and configure the Medusa Stripe plugin. You must map Stripe account keys and configure the webhook secret to handle asynchronous payment events like 'payment_intent.succeeded'.

medusa-config.js
const plugins = [
  {
    resolve: `medusa-payment-stripe`,
    options: {
      api_key: process.env.STRIPE_API_KEY,
      webhook_secret: process.env.STRIPE_WEBHOOK_SECRET,
      capture: true,
    },
  },
];

⚠ Common Pitfalls

  • Forgetting to add the Stripe provider to the 'Regions' settings in the Medusa Admin panel.
  • Using the secret key instead of the webhook signing secret for the webhook_secret field.
3

Implement Headless Cart Management in Next.js

Create a custom hook to manage cart creation and persistence. Store the cart_id in local storage or a cookie to ensure the user's session survives page reloads.

hooks/use-cart.ts
export const useCart = () => {
  const createCart = async () => {
    const { cart } = await medusaClient.carts.create();
    localStorage.setItem('cart_id', cart.id);
    return cart;
  };

  const fetchCart = async (id: string) => {
    const { cart } = await medusaClient.carts.retrieve(id);
    return cart;
  };

  return { createCart, fetchCart };
};

⚠ Common Pitfalls

  • Failing to validate the cart_id on the server side, leading to 404 errors if the cart has expired in the database.
  • Not handling regional currency changes when a user updates their shipping address.
4

Optimize Product Listing Pages with ISR and Cloudinary

Use Next.js Incremental Static Regeneration (ISR) to pre-render product pages. Integrate Cloudinary for dynamic image resizing and WebP conversion to maintain high Core Web Vitals scores.

components/product-card.tsx
import Image from 'next/image';

const ProductCard = ({ product }) => (
  <Image
    src={product.thumbnail}
    alt={product.title}
    width={400}
    height={400}
    loader={({ src, width }) => `https://res.cloudinary.com/demo/image/fetch/w_${width},f_auto/${src}`}
    placeholder="blur"
    blurDataURL={product.placeholder}
  />
);

⚠ Common Pitfalls

  • Over-fetching product data in getStaticProps; only select the fields required for the initial render.
  • Neglecting to set a 'revalidate' timer, causing inventory levels to remain stale on the storefront.
5

Integrate Meilisearch for Real-time Filtering

Sync your Medusa product catalog with Meilisearch. This allows for sub-100ms faceted search and filtering on the frontend without hitting the primary PostgreSQL database.

medusa-config.js
const plugins = [
  {
    resolve: `medusa-plugin-meilisearch`,
    options: {
      config: {
        host: process.env.MEILISEARCH_HOST,
        apiKey: process.env.MEILISEARCH_API_KEY,
      },
      settings: {
        products: {
          indexSettings: {
            searchableAttributes: ['title', 'description', 'variant_sku'],
            filterableAttributes: ['categories', 'handle'],
          },
        },
      },
    },
  },
];

⚠ Common Pitfalls

  • Not configuring 'filterableAttributes' in Meilisearch, which prevents faceted navigation from working.
  • Mismatch between the Medusa product schema and the Meilisearch index structure.
6

Handle Post-Purchase Webhooks for Order Fulfillment

Set up a secure endpoint to listen for Stripe events. Once a payment is confirmed, update the Medusa order status to 'awaiting_fulfillment' to trigger downstream shipping workflows.

app/api/webhooks/stripe/route.ts
export async function POST(req: Request) {
  const body = await req.json();
  const signature = req.headers.get('stripe-signature');

  try {
    const event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET);
    if (event.type === 'payment_intent.succeeded') {
      // Logic to capture Medusa payment
    }
    return new Response('Success', { status: 200 });
  } catch (err) {
    return new Response(`Webhook Error: ${err.message}`, { status: 400 });
  }
}

⚠ Common Pitfalls

  • Testing webhooks without the Stripe CLI, which leads to difficulty debugging payload issues.
  • Missing idempotency keys in the webhook handler, which can cause duplicate order processing.

What you built

By decoupling the frontend from the commerce engine, you have created a high-performance storefront capable of regional scaling. Next steps include implementing AI-generated product descriptions via a Medusa subscriber and setting up automated SEO metadata generation for product variants.