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 thought: that's for companies with a dev team and a venture-backed budget. I thought the same thing when I was running my first store on a hosted platform, paying 2% transaction fees on every order and waiting three weeks for a theme tweak to go live.

Then a developer friend dared me to try building a headless storefront over a long weekend. By Sunday night I had a working product catalog, a cart, and a checkout flow — all loading in under 1.2 seconds on a budget $6/month server. The secret wasn't some magic framework. It was breaking the project into four honest steps and ignoring every distraction that didn't move the needle.

If you can follow a recipe, you can do this. Let's walk through it together.

What "Headless" Actually Means for Your Store

In a traditional platform setup, the storefront (what customers see) and the backend (inventory, orders, pricing) are locked together in one system. Change one, and you risk breaking the other. Headless just means you separate them: your backend exposes data through an API, and your frontend — built however you like — fetches and displays that data.

Next.js is a React framework that handles routing, server-side rendering, and static page generation out of the box. It's the frontend layer most headless stores reach for first, and for good reason: its hybrid rendering model lets you statically generate your product pages (blazing fast, great for SEO) while keeping cart and checkout dynamic.

For the backend, you have real choices. Medusa.js is open-source and self-hostable. Swell, Crystallize, and Vendure are solid mid-market options. Even a simple Shopify store can act as a headless backend through its Storefront API — you pay for the commerce logic, not the storefront. Pick whichever fits your budget; the frontend steps below work with any of them.

What You'll Need Before You Start

No, you don't need to be a senior engineer. But you do need a few things in place:

  • Node.js 18+ installed on your machine (check with node -v in your terminal)
  • A free Vercel account for deployment — their free tier handles up to 100 GB of bandwidth per month, which is more than enough to launch
  • A backend that exposes a product API — for this walkthrough, we'll assume a Medusa.js instance running locally or on a $6/month DigitalOcean droplet
  • About 6–8 hours of focused time, split across two days

If you've never touched a terminal before, budget an extra hour on day one to get comfortable. It's less scary than it looks.

Day One: Scaffold the Project and Pull Real Product Data

Start by creating your Next.js app. Open your terminal and run:

npx create-next-app@latest my-headless-store

Choose the App Router option when prompted — it's the current standard and handles server components cleanly. Say yes to TypeScript if you're comfortable with it; if not, plain JavaScript works fine.

Next, install a lightweight fetch helper. swr (stale-while-revalidate) is a 4 KB library from Vercel that handles client-side data fetching with caching baked in:

npm install swr

Now create a simple API utility file at lib/api.js:

const BASE_URL = process.env.NEXT_PUBLIC_API_URL;

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

export async function getProduct(id) {
  const res = await fetch(`${BASE_URL}/store/products/${id}`);
  if (!res.ok) throw new Error("Product not found");
  return res.json();
}

Add your backend URL to a .env.local file:

NEXT_PUBLIC_API_URL=http://localhost:9000

Then build a quick product listing page at app/products/page.jsx. Fetch the products server-side so they're statically rendered:

import { getProducts } from "@/lib/api";

export default async function ProductsPage() {
  const { products } = await getProducts();
  return (
    <main>
      <h1>Our Products</h1>
      <ul>
        {products.map((p) => (
          <li key={p.id}>
            <a href={`/products/${p.id}`}>{p.title}</a>
            <span> — ${(p.variants[0].prices[0].amount / 100).toFixed(2)}</span>
          </li>
        ))}
      </ul>
    </main>
  );
}

Run npm run dev, open localhost:3000/products, and you should see real product names and prices pulled live from your backend. That moment — seeing your own data render in a page you built — is genuinely satisfying. Pause and enjoy it for a second.

Spend the rest of day one adding a product detail page at app/products/[id]/page.jsx using the same pattern, and dropping in a basic CSS file or a utility-first library like Tailwind (it ships as an option in create-next-app, so you may already have it).

Day Two: Build the Cart and Wire Up Checkout

A cart is just state — a list of items the customer intends to buy. React's useContext hook is all you need to share that state across your whole app without a heavy library.

Create context/CartContext.jsx:

"use client";
import { createContext, useContext, useState } from "react";

const CartContext = createContext(null);

export function CartProvider({ children }) {
  const [items, setItems] = useState([]);

  function addItem(product, variantId) {
    setItems((prev) => {
      const existing = prev.find((i) => i.variantId === variantId);
      if (existing) {
        return prev.map((i) =>
          i.variantId === variantId ? { ...i, qty: i.qty + 1 } : i
        );
      }
      return [...prev, { product, variantId, qty: 1 }];
    });
  }

  function removeItem(variantId) {
    setItems((prev) => prev.filter((i) => i.variantId !== variantId));
  }

  const total = items.reduce(
    (sum, i) => sum + i.product.variants[0].prices[0].amount * i.qty,
    0
  );

  return (
    <CartContext.Provider value={{ items, addItem, removeItem, total }}>
      {children}
    </CartContext.Provider>
  );
}

export const useCart = () => useContext(CartContext);

Wrap your root layout with <CartProvider> in app/layout.jsx, then add an "Add to Cart" button on each product detail page that calls addItem. Build a /cart page that maps over items and shows a running total.

For checkout, the fastest honest path for a weekend project is to hand off to a payment provider's hosted page. Stripe's Payment Links let you create a link per product variant in your Stripe dashboard and redirect the customer there — zero PCI scope on your end, and it takes about 20 minutes to set up. Once you're comfortable, you can replace that with a full Stripe Checkout Session API call that pre-fills the cart contents, but the hosted link approach ships today.

Here's a real example: a candle maker I know launched her headless Next.js store in exactly this way. She had 12 SKUs, used Medusa as her backend, and redirected checkout to Stripe Payment Links. Her store went live on a Sunday. She processed her first order Monday morning, paying $0 in platform fees beyond Stripe's standard 2.9% + 30¢.

Deploy in Under 15 Minutes With Vercel

This part is almost embarrassingly easy. Push your project to a GitHub repository, log into your Vercel account, click "Add New Project," and select your repo. Vercel auto-detects Next.js and configures the build for you.

Before you hit deploy, add your environment variables in the Vercel dashboard under Project Settings → Environment Variables. Paste in your NEXT_PUBLIC_API_URL pointing to your live backend (not localhost). If your backend is on a DigitalOcean droplet, that's the droplet's public IP or domain.

Click deploy. In roughly 90 seconds you'll have a live URL. Vercel's free Hobby plan covers personal projects; their Pro plan at $20/month adds team features and higher bandwidth limits if you grow into them.

A quick performance note: Next.js statically generates your product pages at build time by default. That means a customer hitting your product page gets a pre-rendered HTML file served from a CDN edge node — no server round-trip, no database query in the critical path. In my testing on a 10-product store, Core Web Vitals were consistently strong, with Time to First Byte under 80ms from North American locations. That's hard to match on a traditional hosted platform without paying for premium CDN add-ons.

Three Things to Do Before You Call It Launched

Before you share your URL with the world, run through this short checklist:

  1. Add an og:image meta tag to your root layout. A 1200×630 px image makes your store look legitimate when someone shares a link on social media. Takes 10 minutes, makes a real difference.

  2. Set up a robots.txt and a sitemap. Next.js 13.3+ has a built-in sitemap.js file convention — you can generate a dynamic sitemap from your product API in about 20 lines of code. Google needs this to index your pages properly.

  3. Test checkout on a real device over mobile data, not just your laptop on Wi-Fi. Stripe Payment Links are mobile-optimized, but your cart page might have layout issues on a 375px screen. Fix those before launch, not after your first customer complains.

Do all three and you'll be ahead of a surprising number of small stores that launched without them.

You Built This — What Comes Next

Getting a headless Next.js storefront live in a weekend isn't a shortcut or a hack. It's a legitimate, production-ready architecture that scales from your first sale to your hundred-thousandth without forcing you to replatform. You own the frontend, you choose the backend, and you're not paying a percentage of your revenue to a platform for the privilege of using their theme editor.

The natural next step is adding search. Algolia's free tier covers up to 10,000 records and 10,000 searches per month — more than enough for a growing catalog. This guide on InstantSearch drops into a Next.js project in an afternoon and gives your customers a genuinely fast, filter-as-you-type experience.

You've got the foundation. Build on it.