Next.js Ecommerce: How to Build a Store in 2026

Michał Miszczyszyn

Updated Sep 18, 202614 min read

Next.js ecommerce is no longer a niche: Nike and Sonos run storefronts on it, and thousands of independent brands do too. But "build a store with Next.js" can mean four very different projects: a headless Shopify frontend, a self-hosted open-source backend, a managed commerce platform, or raw Stripe calls. This guide explains why Next.js suits ecommerce, walks through the three architecture decisions that matter, and gives you working Next.js 16 code for two build paths.

🔍

Bias disclosure: We build Your Next Store, a commerce platform with an open-source Next.js storefront. We'll recommend it where it fits, tell you where alternatives win, and flag where our experience colors the analysis.

Why Use Next.js for Ecommerce?

Ecommerce is the most performance-sensitive category on the web. Every 100ms of latency was famously measured to cost Amazon 1% in sales, and search engines use Core Web Vitals as a ranking signal. Next.js is built to make fast pages the default, while still letting you show live prices, stock and carts.

Here's what Next.js 16 gives an online store:

FeatureWhy It Matters for Ecommerce
React Server ComponentsProduct pages render on the server. Descriptions and images ship as HTML, not JavaScript.
Cache Components ("use cache")Cache a product query, a component or a whole page, and tag it so a product update refreshes only what changed.
Partial PrerenderingWith cacheComponents on, each route serves a prerendered static shell instantly and streams the dynamic parts (cart, stock, personalization).
Server ActionsAdd-to-cart and checkout run on the server with no hand-written API routes.
next/imageAVIF/WebP, responsive srcset and lazy loading for product photos.
Metadata APITitles, canonical URLs, Open Graph images and JSON-LD per product.

Beyond speed, three reasons keep coming up when teams choose Next.js for ecommerce development:

  1. One language across the stack. Storefront, server logic and often the backend are TypeScript, so a single team can own the whole store. It's the most common way to do JavaScript ecommerce today.
  2. Full control of the frontend. Theme-based platforms limit you to what the theme exposes. In Next.js, every page, filter and checkout step is your own React code.
  3. Hiring and ecosystem. React is the most widely used frontend library, so Next.js ecommerce developers are easier to find than specialists in a platform's templating language, and component libraries like shadcn/ui cover most storefront UI.

When Next.js is not the right choice

Be honest about the tradeoff: Next.js is a framework, not a store. You still need a commerce backend, and someone has to own the code. If nobody on your team writes React and you don't plan to hire, a hosted builder with themes (Shopify, Wix, Squarespace) will get you selling with less effort. We compare those paths directly in Next.js vs Shopify and WooCommerce vs Next.js.

Next.js Ecommerce Architecture: Three Decisions

Every Next.js store comes down to three choices: where commerce data lives, how payment is taken, and what gets cached. Make them in this order.

1. Choose your commerce backend

Next.js renders the store. Something else has to manage products, variants, inventory, carts, orders and customers. Your realistic options in 2026:

BackendTypeBest ForPricingSelf-Host?
Your Next StoreManaged backend + open-source storefrontFastest launch, AI builder, developer-friendly API$30-360/mo, 0% platform fees (Stripe fees apply)Storefront only
MedusaOpen-source headless commerce (Node.js)Full backend controlFree to self-host, or Medusa CloudYes
SaleorOpen-source headless commerce (GraphQL)Enterprise, multi-channelFree to self-host, or Saleor CloudYes
Vendure / SpreeOpen-source headless commerce (Node.js / Rails)Plugin-based customization, B2BFree to self-hostYes
Shopify (headless)Hosted commerce + Storefront APIExisting Shopify merchants going customPlans from $39/mo, plus fees unless you use Shopify PaymentsNo
Stripe onlyPayment processorA handful of products, no variantsStripe processing fees onlyN/A

When to choose what:

  • Want to launch this week? YNS gives you a managed backend, an open-source Next.js storefront and an AI builder that can generate the store from a chat. You can take over the code whenever you want.
  • Want to own the entire stack? Medusa, Saleor, Vendure or Spree let you self-host everything. Local setup is quick; in production you run the database, backend and deployments yourself (or use their cloud offerings where available).
  • Already on Shopify? Use the Storefront API with a Next.js frontend. Vercel's Next.js Commerce is the reference starter.
  • Selling a few simple products? Selling directly with Stripe might be all you need.

Each of these has a ready-made starter. We compare them, with Next.js versions and licenses, in our roundup of the best Next.js ecommerce templates and free starters. For the wider backend ranking, see best ecommerce platform for developers.

2. Decide how you take payment

You have two patterns, and both keep card data off your server:

  • Hosted checkout (redirect). Stripe Checkout, Shopify checkout or a platform's hosted checkout page. The least work and the lowest PCI scope. You give up some control over the checkout layout.
  • Embedded checkout. Stripe's Payment Element or Express Checkout Element inside your own checkout page. More work, full design control, and wallets like Apple Pay and Google Pay inline.

If you sell into the EU or UK, plan for Strong Customer Authentication (3D Secure) and VAT from day one. Stripe handles SCA challenges in both patterns, and a tax engine such as Stripe Tax can calculate VAT per country. Cross-border tax is covered in how to sell internationally, and checkout UX in our ecommerce checkout optimization guide.

3. Plan rendering and caching

In Next.js 16, turn on Cache Components in next.config.ts. It replaces the Next.js 15 flags experimental.ppr, experimental.dynamicIO and experimental.useCache:

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
cacheComponents: true,
};

export default nextConfig;

With it on, data fetching is dynamic by default and you opt in to caching with "use cache". A rule of thumb for stores:

DataStrategy
Catalog, collections, product content"use cache" + cacheTag("products") + a cacheLife profile like "hours"
Price and stock during a flash saleShorter cacheLife ("minutes" or "seconds"), or leave it dynamic inside <Suspense>
Cart, session, customer accountDynamic (reads cookies), rendered inside <Suspense>
After an admin or webhook updaterevalidateTag("products", "max"): serve stale, refresh in the background
After the shopper's own write (in a Server Action)updateTag("cart"): the next read waits for fresh data

One change trips up many Next.js 15 codebases: the single-argument revalidateTag(tag) is deprecated in Next.js 16. Pass a profile ("max" is recommended) or use updateTag inside Server Actions.

Path 1: A Minimal Next.js Store with Stripe

Before reaching for a platform, here is what a bare-bones Next.js 16 store looks like. It's useful even if you won't ship it, because it shows exactly what a commerce backend abstracts away.

Fetch products from Stripe in a cached, tagged function:

// lib/products.ts
import { cacheLife, cacheTag } from "next/cache";
import Stripe from "stripe";

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function getProducts() {
"use cache";
cacheTag("products");
cacheLife("hours");

const { data } = await stripe.products.list({
active: true,
expand: ["data.default_price"],
});

// Keep only products with a one-off price, and return plain serializable objects
return data.flatMap((product) => {
const price = product.default_price;
if (!price || typeof price === "string" || price.unit_amount === null) return [];
return [
{
id: product.id,
name: product.name,
priceId: price.id,
amount: price.unit_amount, // smallest currency unit, e.g. cents
currency: price.currency,
},
];
});
}

Create a Stripe Checkout Session in a Server Action. Server Actions are public HTTP endpoints, so never trust the price ID the browser sends: check it against your own catalog.

// app/actions.ts
"use server";

import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { getProducts, stripe } from "@/lib/products";

export async function buyNow(priceId: string) {
const product = (await getProducts()).find((p) => p.priceId === priceId);
if (!product) throw new Error("Unknown product");

const origin = (await headers()).get("origin") ?? "http://localhost:3000";
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: [{ price: product.priceId, quantity: 1 }],
success_url: `${origin}/thank-you?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: origin,
});

if (!session.url) throw new Error("Stripe did not return a Checkout URL");
redirect(session.url);
}

Render the catalog. Because getProducts is cached, this page is prerendered into the static shell:

// app/page.tsx
import { buyNow } from "@/app/actions";
import { getProducts } from "@/lib/products";

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

return (
<main>
{products.map((product) => (
<form key={product.id} action={buyNow.bind(null, product.priceId)}>
<h2>{product.name}</h2>
<p>
{new Intl.NumberFormat("en-US", { style: "currency", currency: product.currency }).format(
product.amount / 100,
)}
</p>
<button type="submit">Buy now</button>
</form>
))}
</main>
);
}

Finally, refresh the catalog when you edit products in the Stripe Dashboard, using a webhook Route Handler:

// app/api/stripe-webhook/route.ts
import { revalidateTag } from "next/cache";
import { stripe } from "@/lib/products";

export async function POST(request: Request) {
const signature = request.headers.get("stripe-signature") ?? "";
const body = await request.text();

try {
const event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
if (event.type.startsWith("product.") || event.type.startsWith("price.")) {
revalidateTag("products", "max");
}
return Response.json({ received: true });
} catch {
return new Response("Invalid signature", { status: 400 });
}
}

This works, and it's a legitimate way to sell a few products. But look at what's missing: no cart, no variants, no inventory, no order records you control, no customer accounts, no search, no categories, no shipping rates, no admin. That's the job of a commerce backend, and building all of it yourself takes months.

Path 2: Build on a Managed Backend (YNS)

This is the approach we recommend for most teams. You get a commerce backend (products, variants, carts, orders, inventory, customers, search) that's run for you, and an open-source Next.js 16 storefront you fully control.

Set up the project

Clone the Your Next Store template (MIT):

git clone https://github.com/yournextstore/yournextstore.git my-store
cd my-store && bun install
cp .env.example .env.local

Create a store on Your Next Store, generate an API key in your store's settings, and add it to .env.local:

YNS_API_KEY=your_api_key_here

Start the dev server with bun dev and open localhost:3000. Your store is running with products, cart, checkout and search, all served by the managed backend.

YNS demo storefront showing product grid with categories and search

Fetch products with the Commerce Kit SDK

The template talks to the backend through commerce-kit, a typed client for the YNS REST API. Your YNS_API_KEY authenticates every request. Products, carts and orders live in the backend's PostgreSQL database; Stripe only processes payments.

// lib/commerce.ts
import { Commerce } from "commerce-kit";

export const commerce = Commerce({ token: process.env.YNS_API_KEY });
// Anywhere on the server
const { data: products } = await commerce.productBrowse({ active: true, limit: 12 });
const product = await commerce.productGet({ idOrSlug: "leather-backpack" });

Build product pages with Server Components

This is a trimmed version of the template's product page. Under Cache Components, awaiting params is request-time work, so the page stays a synchronous shell and the product details stream in inside <Suspense>. The data fetch itself is cached:

// app/product/[slug]/page.tsx
import { cacheLife } from "next/cache";
import { notFound } from "next/navigation";
import { Suspense } from "react";
import { commerce } from "@/lib/commerce";

async function getProduct(slug: string) {
"use cache";
cacheLife("minutes");
// productGet throws for an unknown slug, so turn that into a 404
return commerce.productGet({ idOrSlug: slug }).catch(() => null);
}

export default function ProductPage(props: { params: Promise<{ slug: string }> }) {
return (
<Suspense fallback={<ProductSkeleton />}>
<ProductDetails params={props.params} />
</Suspense>
);
}

async function ProductDetails({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const product = await getProduct(slug);
if (!product) notFound();

return (
<main>
<MediaGallery images={product.images} />
<h1>{product.name}</h1>
<AddToCartButton variants={product.variants} />
<RelatedProducts productId={product.id} />
</main>
);
}

Key decisions:

  • Cache the data, not the request. getProduct is cached per slug for minutes, so repeat visits skip the API call while stock and price stay reasonably fresh.
  • Server Components by default. Only the interactive parts (variant picker, add to cart) are Client Components.
  • Structured data. The real page also renders Product and Breadcrumb JSON-LD, which helps you earn rich results in Google.

Add the cart with Server Actions

Add-to-cart is a Server Action. It calls the SDK, which stores the cart in the backend, and keeps the cart ID in an HTTP-only cookie:

// app/cart/actions.ts
"use server";

import { commerce } from "@/lib/commerce";
import { getCartCookieJson, setCartCookie } from "@/lib/cookies";

export async function addToCart(variantId: string, quantity = 1) {
const cartCookie = await getCartCookieJson();
const cart = await commerce.cartUpsert({ cartId: cartCookie?.id, variantId, quantity });

if (cart.id !== cartCookie?.id) {
await setCartCookie({ id: cart.id });
}
return cart;
}

The template's action returns the updated cart, and the cart drawer applies the change locally first so the UI responds instantly. If you render the cart from a cached, tagged query instead, call updateTag("cart") at the end of the action so the shopper sees their own write immediately.

Checkout is a plain link to /checkout. The template forwards that path to the YNS platform's hosted checkout, which takes payment through Stripe, so card data never touches your storefront. After payment the backend creates the order.

Deploy to production

Push the repo to GitHub and import it into Vercel, or use the deploy button:

Deploy with Vercel

💡

The template ships SEO defaults out of the box: a dynamic sitemap, robots.txt, llms.txt, JSON-LD for products and breadcrumbs, and per-product Open Graph metadata. See our ecommerce SEO guide for what to add next.

The AI Builder Alternative (No Code)

Not every store needs to start with code. YNS also has an AI builder: describe your brand, products and aesthetic in a chat, and the AI edits real Next.js code, with a live preview before you publish.

Mascot AI was built this way:

Mascot AI store selling collectible AI mascot figures

The useful part is that you can move between AI and code. A non-technical founder can launch with the AI builder, and a developer can later take over the same Next.js codebase without a rebuild. The same Commerce Kit SDK and managed backend power both paths.

Code-first or AI-first, same stack. Spin up a Next.js 16 storefront with YNS and move between the two as you grow.

Performance: Why Next.js Stores Are Fast

Speed is revenue in ecommerce. Two techniques do most of the work:

Partial Prerendering. When a shopper opens a product page:

  1. The static shell (navigation, layout, cached product content) is served immediately from the CDN
  2. Request-specific parts (cart count, personalized blocks) stream in behind <Suspense> boundaries
  3. The page is usable before the dynamic data arrives

We covered PPR in depth in Partial Prerendering in Next.js. In Next.js 16 it's no longer a separate flag: it's how cacheComponents renders routes.

React Server Components mean you ship less JavaScript. Product details, descriptions and images arrive as HTML, and the browser only downloads JavaScript for interactive elements like the cart.

The result: the YNS template reached a 100/100 Real Experience Score with real products and real payments, and the backend serves a demo store with a million products. For a full checklist, see our ecommerce website speed optimization guide.

YNS store with 1,000,000 products and instant page loads

What Nobody Tells You About Next.js Ecommerce

A few things you only learn after shipping a real store:

  • Plan product variants upfront. Restructuring size, color and material after you have orders is painful on any backend.
  • Images matter more than you think. Product photos are the heaviest assets on the page. Use next/image with a correct sizes prop, and mark the main product image as priority so it doesn't hurt your Largest Contentful Paint.
  • Cache lifetime is a business decision. cacheLife("hours") is fine for most catalogs. If you run flash sales or limited drops, stale prices cause support tickets: shorten the lifetime or revalidate by tag from a webhook.
  • Treat every Server Action as a public endpoint. Validate input (for example with Zod) and look prices up on the server. Never trust amounts sent by the browser.
  • SEO takes months to compound. Your store can be faster than competitors on day one, but rankings take months. Ship structured data, sitemaps and proper metadata at launch.
  • "Headless" means you build the head. Every headless backend (YNS, Medusa, Saleor, Shopify's Storefront API) leaves the storefront UI to you. Templates and AI builders cut that work, but budget time for customization.

Hiring a Next.js Ecommerce Developer

If you're hiring for Next.js ecommerce development rather than building it yourself, check for these skills:

  • App Router and Server Components, not only the older Pages Router
  • Next.js 16 caching: "use cache", cacheTag/cacheLife, and when to use updateTag vs revalidateTag
  • Payments and webhooks: signature verification, idempotent order handling, and SCA if you sell in Europe
  • Commerce data modeling: variants, inventory, multi-currency and tax-inclusive pricing
  • Core Web Vitals: measured on real product pages, not only a landing page

Ask to see a live store they built, and run it through PageSpeed Insights on mobile.

FAQ

Why use Next.js for ecommerce development?

Next.js combines server rendering, fine-grained caching and a static shell with streamed dynamic content, so product pages are fast and search-friendly while cart and stock stay live. You also get one TypeScript codebase, full control over the storefront, and a large React talent pool.

Is Next.js good for ecommerce?

Yes, for teams that can own a React codebase. It's used by large retailers and small brands alike. The catch is that Next.js is only the storefront framework: you still need a commerce backend (a managed one like YNS, a self-hosted one like Medusa, or Shopify's API) and someone to maintain the code.

Can I build an ecommerce website with JavaScript?

Yes. A modern JavaScript ecommerce stack (we break down which layers to build and which to buy) is usually TypeScript end to end: Next.js for the storefront, a Node.js or managed commerce backend for products and orders, and Stripe for payments. Everything in this guide is JavaScript/TypeScript.

What is the best Next.js ecommerce template?

It depends on your backend. We compare nine current options, including Vercel's Next.js Commerce, Medusa, Payload and YNS, in the best Next.js ecommerce templates.

Do I need a database to build a Next.js store?

It depends on your approach. With a managed backend like YNS, the backend stores everything in PostgreSQL and you never manage the database. With Medusa, Saleor, Vendure or Spree you run your own database. A Stripe-only store needs no database, but you also get no cart persistence, search or order management of your own.

How much does it cost to run a Next.js ecommerce site?

At the low end, a Stripe-only store on a free hosting tier costs nothing until you sell (in the US, Stripe's standard card rate is 2.9% + 30¢; rates differ in the EU and UK). YNS starts at $30/month with 0% platform transaction fees, so you only pay Stripe's processing fees. Self-hosted open-source backends are free to license, but you pay for servers and database hosting. Headless Shopify starts at Shopify's plan prices plus any transaction fees. For a full breakdown, see how much an ecommerce website costs.

Can I migrate from Shopify to Next.js?

Yes. Most commerce backends import Shopify product data via CSV, and the main work is building storefront components. You can also go gradually: keep Shopify as the backend and use its Storefront API with a Next.js frontend. See migrating from Shopify to headless commerce.


The best stores in 2026 won't be the ones with the most features. They'll be the ones that load before the customer can blink. Next.js gives you that speed. Which backend you pair it with is up to you.