Set Up a Headless Storefront With Next.js in a Weekend

by Emma Rodriguez
Set Up a Headless Storefront With Next.js in a Weekend

You've probably seen the phrase "headless commerce" floating around and quietly wondered whether it's for real businesses or just something agencies pitch to justify bigger invoices. Fair question. When I was running my second store, I spent three weeks convinced it was the latter — until a slow Shopify theme was costing me roughly 1.2 seconds of load time and, according to my analytics, about 11% of mobile checkouts.

That's when I actually sat down and built a headless front end over a long weekend. It wasn't magic, but it worked. And if you've got two days, a basic comfort with JavaScript, and a backend you already trust, you can do the same thing.

This tutorial walks you through the whole setup: picking your commerce backend, scaffolding a Next.js project, wiring up product data, and shipping something real. No PhD in distributed systems required.

Why "Headless" Is Just a Fancy Word for Separation of Concerns

Here's the plain-English version: in a traditional platform, your storefront design (the "head") and your commerce logic — inventory, cart, checkout, orders — live in the same system. Headless just means you split them apart. Your commerce backend does the data heavy-lifting via an API, and your front end (Next.js, in our case) fetches that data and renders whatever experience you want.

Why does that matter for a small store? Three reasons that actually show up in your numbers:

  1. Speed. Next.js pre-renders pages at build time, so a product page can load in under 500 ms instead of waiting on a theme server to assemble HTML on every request.
  2. Flexibility. You own the markup. Want a custom bundle builder, a quiz that recommends products, or a landing page that looks nothing like a typical store? You're not fighting a theme editor.
  3. Cost control. Many headless-friendly backends (Medusa, Vendure, even WooCommerce with its REST API) are open-source. Your hosting bill for a Next.js site on Vercel's free tier is $0 until you're doing serious traffic.

The trade-off is real: you're taking on more technical responsibility. But "more" here means maybe four to six hours of setup, not a full engineering team.

Choosing a Commerce Backend You Won't Regret

Before you write a single line of Next.js code, you need somewhere to store products, handle carts, and process payments. Your options roughly fall into three buckets:

Self-hosted open source — Medusa.js and Vendure are the two I'd point a small operator toward. Medusa is particularly friendly: you get a Node.js API server running locally in about 10 minutes with npx create-medusa-app, and it ships with Stripe, PayPal, and manual payment integrations out of the box. Hosting it on Railway or Render runs about $7–$15/month for a starter instance.

Existing platform API — If you're already on WooCommerce, BigCommerce, or a similar platform, you don't have to migrate. Both expose REST and GraphQL APIs you can query from Next.js. You keep your existing order history, customer accounts, and payment setup. This is the lowest-risk starting point if you have an established store.

Headless SaaS — Crystallize, Swell, and similar tools are built API-first. Pricing usually starts around $39–$99/month. They're worth it if you want a managed backend and don't want to think about server maintenance.

For this tutorial I'll use Medusa as the backend example, but the Next.js patterns are identical regardless of which API you point at.

Scaffolding Your Next.js Storefront in About an Hour

Open your terminal and run:

npx create-next-app@latest my-storefront --typescript --app
cd my-storefront

That gives you a Next.js 14 project using the App Router, which is what you want for 2024 and beyond. TypeScript is optional but saves you from a category of bugs that will otherwise show up at midnight before a sale.

Next, install the Medusa JS client (swap this for your backend's SDK if you're using something else):

npm install @medusajs/medusa-js

Create a small utility file at lib/medusa.ts:

import Medusa from "@medusajs/medusa-js";

export const medusa = new Medusa({
  baseUrl: process.env.NEXT_PUBLIC_MEDUSA_URL ?? "http://localhost:9000",
  maxRetries: 3,
});

Add NEXT_PUBLIC_MEDUSA_URL to your .env.local file pointing at your running Medusa instance. That's your data layer done.

Building a Product Listing Page

Create app/products/page.tsx. Because this is a Server Component in the App Router, you can fetch data directly — no useEffect, no loading spinners on first paint:

import { medusa } from "@/lib/medusa";

export default async function ProductsPage() {
  const { products } = await medusa.products.list({ limit: 24 });

  return (
    <main className="grid grid-cols-2 gap-4 p-6 md:grid-cols-4">
      {products.map((p) => (
        <a key={p.id} href={`/products/${p.handle}`} className="group">
          <img
            src={p.thumbnail ?? "/placeholder.png"}
            alt={p.title}
            className="w-full rounded-lg object-cover aspect-square"
          />
          <p className="mt-2 font-medium">{p.title}</p>
          <p className="text-sm text-gray-500">
            From ${((p.variants[0]?.prices[0]?.amount ?? 0) / 100).toFixed(2)}
          </p>
        </a>
      ))}
    </main>
  );
}

Nothing fancy — but this page will score a Largest Contentful Paint under 1 second on a standard Vercel deployment because Next.js renders it server-side and serves it from an edge node close to your visitor.

Building a Product Detail Page

Create app/products/[handle]/page.tsx:

import { medusa } from "@/lib/medusa";
import { notFound } from "next/navigation";

export default async function ProductPage({ params }: { params: { handle: string } }) {
  const { products } = await medusa.products.list({ handle: params.handle });
  const product = products[0];
  if (!product) notFound();

  const price = (product.variants[0]?.prices[0]?.amount ?? 0) / 100;

  return (
    <main className="max-w-4xl mx-auto p-6 grid md:grid-cols-2 gap-8">
      <img src={product.thumbnail ?? "/placeholder.png"} alt={product.title} className="rounded-xl w-full" />
      <div>
        <h1 className="text-2xl font-bold">{product.title}</h1>
        <p className="text-xl mt-2">${price.toFixed(2)}</p>
        <p className="mt-4 text-gray-600">{product.description}</p>
        <button className="mt-6 w-full bg-black text-white py-3 rounded-lg">
          Add to Cart
        </button>
      </div>
    </main>
  );
}

The "Add to Cart" button needs client-side state, so you'd turn it into a "use client" component and call medusa.carts.lineItems.create(cartId, { variant_id, quantity: 1 }). That's a solid next step once the basic pages are working.

Deploying to Vercel in About 15 Minutes

This part is genuinely fast. Push your project to a GitHub repo, then:

  1. Go to vercel.com, click Add New Project, and import your repo.
  2. Add your environment variable (NEXT_PUBLIC_MEDUSA_URL) in the Vercel dashboard under Settings → Environment Variables.
  3. Click Deploy.

Vercel detects Next.js automatically and configures everything. Your first deploy usually finishes in 90–120 seconds. You get a live URL, automatic HTTPS, and global CDN distribution at no cost on the Hobby plan.

If your Medusa backend is running on Railway or Render, make sure its URL is publicly accessible (not localhost) before you deploy. That's the #1 gotcha I see people hit on their first try.

A quick performance sanity check: run your new product listing URL through PageSpeed Insights. A well-built Next.js storefront with static or server-rendered pages routinely scores 90+ on mobile — compared to 55–70 for a typical theme-based store with several third-party scripts loaded.

Three Things to Do Before You Call It Live

You've got pages rendering and data flowing. Before you point your domain at this and call it production, knock out these three things:

1. Add basic SEO metadata. Next.js App Router makes this easy. Export a generateMetadata function from each page that returns a title and description pulled from your product data. Without this, your product pages are invisible to search engines.

2. Set up error boundaries and a 404 page. The notFound() call in the product detail page already handles missing handles, but add a proper not-found.tsx file in your app/ directory so visitors see something friendly instead of a blank screen. If you run into issues, this guide on fixing error 404 in WordPress covers similar troubleshooting principles that apply across platforms.

3. Test checkout on a real device. If you're using Medusa's hosted checkout or redirecting to Stripe Payment Links, open your phone and walk through an actual purchase with a test card. Mobile checkout abandonment averages 85% across e-commerce — you want to catch any friction before real customers do.

Do you have Google Analytics or a privacy-friendly alternative like Plausible set up? Add it now, before launch, so you have a baseline to measure against. Two weeks of pre-launch data is worth more than two months of post-launch guessing.

You've Got a Headless Storefront — What's Next?

Here's what I love about where you are right now: you own the entire front end. That means the next feature — a product comparison table, a loyalty points widget, a custom size guide that actually fits your brand — is just a component away. You're not waiting on a plugin marketplace or a theme update.

The stack you just built (Next.js + a headless commerce API + Vercel) is the same architecture powering stores doing $10M+ a year. The difference between you and them isn't the technology — it's time and iteration.

Your concrete next step: get the cart working end-to-end. Wire up medusa.carts.create() on first visit, store the cart ID in a cookie, and build a small cart drawer component. That's probably another three to four hours of work, and once it's done, you have a fully functional storefront you built yourself.

You've already done the hard part. The rest is just building.