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 heard the word "headless" thrown around in e-commerce circles and thought, that's for companies with engineering teams, not me. I said the same thing when I was running my first store on a hosted platform, paying transaction fees I hated and hitting walls every time I wanted a custom checkout flow.

Then I spent one Saturday actually building a headless storefront with Next.js — not a toy demo, but a real product-listing page connected to live inventory data — and I realized the gap between "sounds scary" and "actually works" is about 12 focused hours and a clear checklist.

This tutorial walks you through exactly that checklist. By Sunday evening you can have a functioning storefront shell that fetches products, renders pages fast enough to score above 90 on Core Web Vitals, and costs you roughly $0/month to host on Vercel's free tier.

Why Headless Commerce Makes Sense for Small Stores Right Now

Headless just means your frontend (what shoppers see) is decoupled from your backend (where products, orders, and inventory live). Instead of one monolithic platform doing everything, you pick the best tool for each job and connect them with APIs.

Why does that matter to you specifically?

  • Speed. Static or server-rendered Next.js pages routinely load in under 1 second. A 1-second improvement in load time correlates with a 7% lift in conversions — that's not a made-up stat, it comes from Akamai research cited widely in web performance circles.
  • Cost control. Shopify's cheapest plan is $39/month plus 2% transaction fees if you use a third-party gateway. A headless setup using a free-tier backend (more on that below) plus Vercel hosting can run under $10/month once you're past the hobby stage, and $0 while you're testing.
  • No platform lock-in. When you own the frontend code, switching backends later is a config change, not a rebuild.

None of that requires a full engineering team. It requires you, a code editor, and a free weekend.

What You'll Actually Build (and What You Won't)

Let's be honest about scope so you don't hit Sunday night feeling like you failed.

You will build:

  • A Next.js 14 app with App Router
  • A product listing page that fetches from a real API
  • A product detail page with dynamic routing
  • Static generation for fast page loads
  • Deployment to Vercel in one command

You won't build this weekend:

  • A full checkout (that's a Part 2 — cart state, Stripe integration, and order webhooks deserve their own tutorial)
  • A custom CMS
  • Authentication

Keeping scope tight is how you actually finish. A working product catalog you can show people is infinitely more useful than a half-built checkout nobody can use.

The Stack: Picking Tools That Won't Fight Each Other

For the backend data source, you have a few solid options that work well with Next.js without forcing you into a long-term contract:

Medusa.js (self-hosted, open source) — Free to run locally or on a $6/month DigitalOcean droplet. Has a REST API and a growing JS SDK. Good fit if you want full control.

Vendure (self-hosted, open source) — GraphQL-native, TypeScript throughout. Slightly steeper learning curve but excellent for stores that will grow past 1,000 SKUs.

A headless CMS with product fields (Sanity, Hygraph) — If you're selling a small catalog (under 50 products), you can model products in a CMS and skip a dedicated commerce backend entirely. Sanity's free tier handles up to 10 GB of data and 2 users.

For this tutorial I'll use Medusa.js because its REST endpoints are easy to read, the local setup takes about 10 minutes, and you can swap it for something else later without touching your Next.js components.

Step-by-Step: From Zero to Running Storefront

1. Spin up your Medusa backend (Saturday morning, ~45 minutes)

Open your terminal and run:

npx create-medusa-app@latest my-store

The CLI will ask you a few questions — accept the defaults. It installs a Node/Express server with a PostgreSQL database (SQLite in dev mode, so no Postgres setup needed locally).

Once it's done, seed the demo data:

cd my-store
medusa seed --seed-file=data/seed.json

You now have ~20 sample products available at http://localhost:9000/store/products. Open that URL in your browser. You should see a JSON response with product titles, thumbnails, variants, and prices. That's your API.

2. Create your Next.js app (Saturday late morning, ~30 minutes)

In a separate terminal window:

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

Say yes to TypeScript and Tailwind — both will save you time, not add to it. Skip the experimental features for now.

Inside your new project, create a file called lib/medusa.ts:

const MEDUSA_BASE = process.env.NEXT_PUBLIC_MEDUSA_URL ?? "http://localhost:9000";

export async function getProducts() {
  const res = await fetch(`${MEDUSA_BASE}/store/products`, {
    next: { revalidate: 60 },
  });
  if (!res.ok) throw new Error("Failed to fetch products");
  return res.json();
}

export async function getProduct(id: string) {
  const res = await fetch(`${MEDUSA_BASE}/store/products/${id}`, {
    next: { revalidate: 60 },
  });
  if (!res.ok) throw new Error("Failed to fetch product");
  return res.json();
}

The next: { revalidate: 60 } option tells Next.js to cache the response and refresh it every 60 seconds — that's Incremental Static Regeneration in one line. Your pages load at static speed but stay fresh.

3. Build the product listing page (Saturday afternoon, ~2 hours)

Replace the contents of app/page.tsx with something like this:

import { getProducts } from "@/lib/medusa";
import Link from "next/link";
import Image from "next/image";

export default async function HomePage() {
  const { products } = await getProducts();

  return (
    <main className="max-w-5xl mx-auto px-4 py-10">
      <h1 className="text-3xl font-bold mb-8">Our Products</h1>
      <div className="grid grid-cols-2 md:grid-cols-3 gap-6">
        {products.map((p: any) => (
          <Link key={p.id} href={`/products/${p.id}`} className="group">
            <div className="aspect-square relative bg-gray-100 rounded-lg overflow-hidden">
              {p.thumbnail && (
                <Image
                  src={p.thumbnail}
                  alt={p.title}
                  fill
                  className="object-cover group-hover:scale-105 transition-transform"
                />
              )}
            </div>
            <p className="mt-2 font-medium">{p.title}</p>
            <p className="text-sm text-gray-500">
              From ${(p.variants[0]?.prices[0]?.amount / 100).toFixed(2)}
            </p>
          </Link>
        ))}
      </div>
    </main>
  );
}

Not glamorous, but it works and it's yours to style however you want.

4. Add the product detail page (Saturday evening, ~1.5 hours)

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

import { getProduct, getProducts } from "@/lib/medusa";
import Image from "next/image";

export async function generateStaticParams() {
  const { products } = await getProducts();
  return products.map((p: any) => ({ id: p.id }));
}

export default async function ProductPage({ params }: { params: { id: string } }) {
  const { product } = await getProduct(params.id);
  const price = product.variants[0]?.prices[0]?.amount / 100;

  return (
    <main className="max-w-4xl mx-auto px-4 py-10">
      <div className="grid md:grid-cols-2 gap-10">
        <div className="aspect-square relative bg-gray-100 rounded-xl overflow-hidden">
          {product.thumbnail && (
            <Image src={product.thumbnail} alt={product.title} fill className="object-cover" />
          )}
        </div>
        <div>
          <h1 className="text-2xl font-bold mb-2">{product.title}</h1>
          <p className="text-xl font-semibold mb-4">${price?.toFixed(2)}</p>
          <p className="text-gray-600 leading-relaxed">{product.description}</p>
          <button className="mt-6 w-full bg-black text-white py-3 rounded-lg font-medium hover:bg-gray-800 transition-colors">
            Add to Cart
          </button>
        </div>
      </div>
    </main>
  );
}

generateStaticParams pre-renders every product page at build time. A catalog of 200 products builds in under 30 seconds on a modern laptop and serves from the CDN edge — no database round-trips per visitor.

5. Deploy to Vercel (Sunday morning, ~20 minutes)

Push your my-storefront folder to a GitHub repo, then:

  1. Go to vercel.com, click Add New Project, and import your repo.
  2. Add one environment variable: NEXT_PUBLIC_MEDUSA_URL pointing to wherever your Medusa instance is running (for production, that DigitalOcean droplet mentioned earlier).
  3. Click Deploy.

Vercel detects Next.js automatically. Your first deploy takes about 90 seconds. You get a live HTTPS URL, a global CDN, and automatic redeploys on every git push.

A Real-World Example: What This Looks Like for a Small Candle Store

Imagine you sell 35 candle SKUs. On Shopify Basic you're paying $39/month plus 2% on every sale through PayPal. On this stack:

  • Medusa on a $6/month DigitalOcean droplet
  • Vercel free tier (handles up to 100 GB bandwidth/month)
  • Stripe direct integration (1.5% lower effective rate than Shopify's third-party fee)

At $5,000/month in revenue, that's roughly $100/month saved on fees alone — $1,200/year you keep. And your product pages load in 800ms instead of 3.2 seconds, which matters more than people realize when someone's browsing on a phone with mediocre signal.

Is the setup more work than clicking through a Shopify theme? Yes. Is it the kind of work you can learn in a weekend and own forever? Also yes.

Three Things to Do Before You Close This Tab

  1. Run npx create-medusa-app@latest right now — even if you only get through the seed step today, you'll have something real to come back to.
  2. Bookmark the Next.js App Router docs — specifically the Data Fetching and Static Generation sections. You'll reference them a lot this weekend.
  3. Write down the one page that matters most to your store — product listing, a landing page, a blog — and commit to finishing just that one page before moving on.

You don't need to boil the ocean. A working product catalog that's fast, cheap to run, and fully under your control is a genuinely useful thing to have. The checkout, the cart, the customer accounts — those come next, and they're all buildable with the same stack.

You've already done harder things than this. Go start the install.