Ever felt trapped by traditional eCommerce platforms? You are not alone. Modern retailers face a tough choice between accepting platform limitations or breaking free with headless architecture. In this guide, you will learn how to build an eCommerce store with a headless CMS using three powerful tools that work magic together—Strapi for content management, Next.js for your storefront, and Stripe for payments.
This approach splits your customer-facing presentation from your backend systems, giving you the freedom to create exactly what your business needs. Headless commerce architecture lets you craft precise customer experiences while keeping the rock-solid stability enterprise commerce demands.
You will build every essential piece: product catalogs, inventory systems, beautiful storefronts, secure checkouts, and deployment strategies. Think of Strapi as your digital warehouse, managing products through an intuitive dashboard. Next.js becomes your virtual storefront, serving lightning-fast pages to customers, while Stripe handles payments with bank-grade security.
In brief:
- A headless eCommerce architecture separates your frontend from your backend, giving you unprecedented flexibility to craft custom shopping experiences while maintaining enterprise-grade stability.
- The Strapi-Next.js-Stripe stack provides the perfect balance between development speed and customization, offering faster launch times than custom solutions while providing more flexibility than closed platforms like Shopify.
- This approach delivers significant performance benefits with Next.js's optimized rendering, resulting in faster page loads that can reduce bounce rates by up to 30% compared to traditional systems.
- You will gain complete control over your business logic and customer experience while leveraging production-ready tools for content management, frontend development, and secure payment processing.
Why Use Strapi, Next.js, and Stripe for Headless Commerce
Strapi, Next.js, and Stripe form a powerful trio that overcomes the limitations of traditional platforms. This combination provides unmatched flexibility, speed, and security—qualities that monolithic systems often lack. Strapi, as a headless CMS, enables businesses to fully customize their storefronts and backends, adapting to specific business needs rather than being limited by rigid frameworks.
Strapi’s open-source nature offers complete control without vendor lock-in, unlike platforms such as Shopify or Prismic. Strapi’s Role-Based Access Control allows content editors, marketing teams, and developers to manage their respective tasks with proper permissions. Strapi’s draft and publish workflow also supports preparation and testing, ensuring campaigns and product updates do not disrupt live stores.
Next.js enhances performance with server-side rendering and static site generation, ensuring fast-loading product pages. Its hybrid rendering options optimize performance for static listings and dynamic product details.
Stripe simplifies payment processing with secure, developer-friendly APIs. It handles everything from credit cards to digital wallets and buy-now-pay-later options, improving conversions and reducing abandoned carts.
The Mug & Snug case study shows how this stack helped launch a social commerce platform quickly and efficiently. For startups and growing businesses, this combination offers scalability and customization without the drawbacks of traditional systems.
How to Build an eCommerce Store Backend with Strapi
Strapi gives you total control over your product data, inventory, and business rules while staying flexible enough to work with any frontend. You will create models for products and categories, set up secure access permissions, and connect payment processing through Stripe.
Think of your backend as both the inventory management system and the data source powering your Next.js storefront. This separation lets content editors manage products independently while developers craft exceptional customer experiences. The API-first approach provides the essential foundation modern stores need, with room for custom business logic that off-the-shelf platforms can't handle.
Launch a Minimal Storefront in 15 Minutes
Nothing builds confidence like seeing something working quickly. Start by creating your backend:
1npx create-strapi-app@latest my-store --quickstart
Once launched, go to the admin panel and create a Product collection type with these fields: name (Text, required), description (Rich Text), price (Number, required), image (Media, single), and slug (UID, generated from name). Add three sample products to test your setup.
Create a basic Next.js frontend:
1npx create-next-app@latest store-frontend --typescript
2cd store-frontend
3npm install axios
Connect your frontend with an API utility that fetches and displays products:
1// utils/api.ts
2import axios from 'axios';
3
4const API_URL = process.env.NEXT_PUBLIC_STRAPI_API_URL || 'http://localhost:1337';
5
6export async function getProducts() {
7 try {
8 const response = await axios.get(`${API_URL}/api/products?populate=image`);
9 return response.data.data;
10 } catch (error) {
11 console.error('Error fetching products:', error);
12 return [];
13 }
14}
Create a product listing component:
1// components/ProductList.tsx
2import React from 'react';
3import Image from 'next/image';
4import { getProducts } from '../utils/api';
5
6export default function ProductList({ products }) {
7 return (
8 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
9 {products.map((product) => (
10 <div key={product.id} className="border rounded p-4">
11 {product.image.data && (
12 <Image
13 src={`${process.env.NEXT_PUBLIC_STRAPI_API_URL}${product.image.data.url}`}
14 width={300}
15 height={300}
16 alt={product.name}
17 />
18 )}
19 <h2 className="text-xl font-bold">{product.name}</h2>
20 <p className="text-gray-700">${product.price.toFixed(2)}</p>
21 <button className="bg-blue-500 text-white px-4 py-2 rounded mt-2">
22 Add to Cart
23 </button>
24 </div>
25 ))}
26 </div>
27 );
28}
For payments, add your Stripe test keys to environment variables and install the Stripe package:
1npm install @stripe/stripe-js
Deploy to Vercel using their GitHub integration for instant sharing.
Test the complete flow by adding products in your admin, checking they appear on your frontend, and processing a test payment. This minimal setup demonstrates core concepts while giving you a working foundation for your eCommerce store built with a headless CMS.
Configure Strapi and Enable Core Plugins
Getting production-ready requires proper configuration and essential plugins. Node.js Active LTS or Maintenance LTS versions, currently versions 20 and 22, are supported for Strapi. You will also need a database; SQLite is suitable for development, while PostgreSQL or MySQL are recommended for production. Reviewing MySQL vs. PostgreSQL trade-offs early will save headaches when you scale.
Set up your database connection in config/database.js
using environment variables for client, host, port, database, user, password, and SSL settings.
To create an admin user with a strong password in Strapi, log into your Strapi dashboard, navigate to 'Settings', and under the 'Administration Panel', click 'Users'. Use the 'Invite New User' button to add details like first name, last name, email and select a role such as Super Admin. Ensure the password is strong by using a mix of uppercase, lowercase, numbers, and special characters. Strapi doesn't support two-factor authentication natively, so you will need to integrate an external 2FA provider if desired.
Add the GraphQL plugin for flexible data querying through GraphQL APIs, which is especially helpful for complex product relationships:
1npm install @strapi/plugin-graphql
The Users & Permissions plugin in Strapi manages customer accounts and API security, allowing configuration for customer registration, authentication, and role-based access. It supports a full authentication process using JSON Web Tokens (JWT) and an access-control list (ACL) strategy for managing permissions among user groups.
Configure CORS settings in config/middleware.js
as follows:
1// config/middleware.js
2module.exports = [
3 'strapi::errors',
4 {
5 name: 'strapi::security',
6 config: {
7 contentSecurityPolicy: {
8 useDefaults: true,
9 directives: {
10 'connect-src': ["'self'", 'https:'],
11 'img-src': ["'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'your-s3-bucket.amazonaws.com'],
12 },
13 },
14 },
15 },
16 {
17 name: 'strapi::cors',
18 config: {
19 origin: ['http://localhost:3000', 'https://your-frontend-domain.com'],
20 methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
21 headers: ['Content-Type', 'Authorization', 'Origin', 'Accept'],
22 credentials: true,
23 },
24 },
25 'strapi::poweredBy',
26 'strapi::logger',
27 'strapi::query',
28 'strapi::body',
29 'strapi::session',
30 'strapi::favicon',
31 'strapi::public',
32];
The Upload plugin manages product images with appropriate file size limits. For production, connect to cloud storage like AWS S3 or Cloudinary for efficient media delivery.
Watch out for common issues: set unique ports if running multiple instances, configure CORS for your frontend domains, and implement rate limiting to prevent API abuse. Create environment-specific configurations for development, staging, and production with different database connections, API URLs, and security settings.
Model Product, Category, and Page Data
Your data model forms the foundation of your store's functionality. Here's how to define your Product model using the Strapi content-type builder:
1// Example of Product model schema structure
2// Path: src/api/product/content-types/product/schema.json
3{
4 "kind": "collectionType",
5 "collectionName": "products",
6 "info": {
7 "singularName": "product",
8 "pluralName": "products",
9 "displayName": "Product"
10 },
11 "options": {
12 "draftAndPublish": true
13 },
14 "attributes": {
15 "title": {
16 "type": "string",
17 "required": true
18 },
19 "description": {
20 "type": "richtext"
21 },
22 "price": {
23 "type": "integer",
24 "required": true,
25 "min": 0
26 },
27 "sku": {
28 "type": "string",
29 "unique": true
30 },
31 "stripeProductId": {
32 "type": "string"
33 },
34 "images": {
35 "type": "media",
36 "multiple": true
37 },
38 "inventory": {
39 "type": "integer",
40 "default": 0
41 },
42 "status": {
43 "type": "enumeration",
44 "enum": ["draft", "published", "discontinued"],
45 "default": "draft"
46 },
47 "categories": {
48 "type": "relation",
49 "relation": "manyToMany",
50 "target": "api::category.category",
51 "inversedBy": "products"
52 }
53 }
54}
Create a Category content type with name (Text, required), slug (UID), description (Rich Text), parentCategory (Relation to Category) for nested categories, and image (Media, single). Set up a many-to-many relationship between Products and Categories, letting products belong to multiple categories for flexible merchandising.
For variants, create a reusable component:
1// Path: src/components/product/variant.json
2{
3 "collectionName": "components_product_variants",
4 "info": {
5 "displayName": "Variant",
6 "description": "Product variants like size and color"
7 },
8 "options": {},
9 "attributes": {
10 "name": {
11 "type": "string",
12 "required": true
13 },
14 "options": {
15 "type": "json",
16 "required": true
17 },
18 "priceAdjustment": {
19 "type": "integer",
20 "default": 0
21 }
22 }
23}
Add a Page content type for non-product content with title (Text, required), slug (UID), content (Dynamic Zone), and seo (Component) for meta information. Use the visual interface to create these relationships and manage the Draft/Publish workflow to ensure only complete and reviewed content is published.
Best practices for product management include consistent naming conventions, clear taxonomies, and standardized data entry. Create validation rules for required fields and acceptable values. Use components for repeatable structures like product specifications to maintain consistency across your catalog.
Add Secure Checkout and Order Management
Security matters most when handling payments. Never expose Stripe secret keys to the client; keep all payment processing safely on your backend.
Create a checkout endpoint in Strapi:
1// Path: src/api/order/controllers/order.js
2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
3
4module.exports = {
5 async createCheckoutSession(ctx) {
6 const { cartItems, customerEmail } = ctx.request.body;
7
8 if (!cartItems || !cartItems.length || !customerEmail) {
9 return ctx.badRequest('Missing required fields');
10 }
11
12 try {
13 // Format line items for Stripe
14 const lineItems = await Promise.all(cartItems.map(async (item) => {
15 // Fetch product from database to verify price
16 const product = await strapi.entityService.findOne(
17 'api::product.product',
18 item.id,
19 { fields: ['title', 'price', 'stripeProductId'] }
20 );
21
22 if (!product) {
23 throw new Error(`Product with ID ${item.id} not found`);
24 }
25
26 return {
27 price_data: {
28 currency: 'usd',
29 product_data: {
30 name: product.title,
31 },
32 unit_amount: product.price, // Price in cents
33 },
34 quantity: item.quantity,
35 };
36 }));
37
38 // Create Stripe checkout session
39 const session = await stripe.checkout.sessions.create({
40 customer_email: customerEmail,
41 payment_method_types: ['card'],
42 line_items: lineItems,
43 mode: 'payment',
44 success_url: `${process.env.FRONTEND_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
45 cancel_url: `${process.env.FRONTEND_URL}/cart`,
46 });
47
48 // Create order record in Strapi
49 await strapi.entityService.create('api::order.order', {
50 data: {
51 stripeSessionId: session.id,
52 customerEmail,
53 totalAmount: session.amount_total,
54 status: 'pending',
55 items: cartItems,
56 },
57 });
58
59 return { sessionId: session.id, url: session.url };
60 } catch (error) {
61 console.error('Checkout error:', error);
62 return ctx.badRequest('Checkout failed', { error: error.message });
63 }
64 }
65};
Set up a route for your checkout endpoint:
1// Path: src/api/order/routes/custom-routes.js
2module.exports = {
3 routes: [
4 {
5 method: 'POST',
6 path: '/orders/checkout',
7 handler: 'order.createCheckoutSession',
8 config: {
9 policies: [],
10 },
11 },
12 ],
13};
Create an Order collection type with fields like stripeSessionId (Text, unique), customerEmail (Email), totalAmount (Number), status (Enumeration), items (JSON), and shippingAddress (Component) to track order progress.
Implement a webhook handler to process Stripe events:
1// Path: src/api/order/controllers/webhook.js
2module.exports = {
3 async handleStripeWebhook(ctx) {
4 const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
5 const signature = ctx.request.headers['stripe-signature'];
6 const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
7
8 let event;
9 try {
10 event = stripe.webhooks.constructEvent(
11 ctx.request.body.toString(),
12 signature,
13 webhookSecret
14 );
15 } catch (err) {
16 console.error('Webhook signature verification failed', err);
17 return ctx.badRequest('Webhook signature verification failed');
18 }
19
20 // Handle the event
21 switch (event.type) {
22 case 'checkout.session.completed':
23 const session = event.data.object;
24
25 // Update order status
26 await strapi.entityService.update('api::order.order', {
27 filters: { stripeSessionId: session.id },
28 data: {
29 status: 'paid',
30 },
31 });
32 break;
33
34 // Handle other event types as needed
35 }
36
37 return { received: true };
38 }
39};
Set up webhook handlers for Stripe events to update order status automatically. Configure webhook endpoints in your Stripe dashboard with proper signature verification. Strapi emphasizes security by enforcing HTTPS, verifying webhook signatures, and validating input. It integrates with Stripe for payment processing, using Stripe's hosted checkout pages to handle sensitive payment data, which supports PCI compliance securely. Strapi ensures sensitive data is kept off its servers by utilizing Stripe's tokenization methods.
How to Build the Storefront with Next.js
Next.js shines for commerce with its flexible rendering options, built-in routing, and performance optimizations. Its hybrid approach balances SEO benefits with dynamic content, perfect for product pages that need both search visibility and real-time inventory updates. When building an eCommerce store with a headless CMS, Next.js offers the tools to create a responsive and engaging frontend.
Your Next.js frontend connects to the Strapi API for product data, shopping cart functionality, and checkout. This API-driven design delivers exceptional customer experiences while keeping the freedom to customize every aspect of your storefront.
Scaffold the Storefront and Fetch Product Data
Start with TypeScript for a better development experience and fewer bugs. Create a central API utility file to handle all your Strapi data fetching.
1// utils/api.ts
2import { Product } from '../types/product';
3
4const API_URL = process.env.NEXT_PUBLIC_STRAPI_API_URL;
5
6export async function fetchProducts(): Promise<Product[]> {
7 const res = await fetch(`${API_URL}/products?populate=*`);
8
9 if (!res.ok) {
10 throw new Error('Failed to fetch products');
11 }
12
13 const data = await res.json();
14 return data.data;
15}
16
17export async function fetchProductBySlug(slug: string): Promise<Product> {
18 const res = await fetch(`${API_URL}/products?filters[slug][$eq]=${slug}&populate=*`);
19
20 if (!res.ok) {
21 throw new Error('Failed to fetch product');
22 }
23
24 const data = await res.json();
25 return data.data[0];
26}
Use SWR for data fetching; it adds automatic caching, revalidation, and error handling. Build dynamic product pages using Next.js routes that create SEO-friendly URLs matching your product slugs.
1// pages/products/[slug].tsx
2import { GetStaticProps, GetStaticPaths } from 'next';
3import { useRouter } from 'next/router';
4import { fetchProductBySlug, fetchProducts } from '../../utils/api';
5import ProductDetail from '../../components/ProductDetail';
6
7export default function ProductPage({ product, error }) {
8 const router = useRouter();
9
10 // Show loading state during ISR revalidation
11 if (router.isFallback) {
12 return <div>Loading...</div>;
13 }
14
15 return <ProductDetail product={product} />;
16}
17
18export const getStaticPaths: GetStaticPaths = async () => {
19 const products = await fetchProducts();
20
21 const paths = products.map((product) => ({
22 params: { slug: product.slug },
23 }));
24
25 return { paths, fallback: 'blocking' };
26};
27
28export const getStaticProps: GetStaticProps = async ({ params }) => {
29 try {
30 const product = await fetchProductBySlug(params.slug as string);
31
32 return {
33 props: { product },
34 // ISR: Revalidate page every 10 minutes
35 revalidate: 600,
36 };
37 } catch (error) {
38 return { notFound: true };
39 }
40};
Implement Incremental Static Regeneration (ISR) for product pages to balance SEO with fresh data. Create reusable components for product cards, images, and information displays. This modular approach makes your codebase easier to manage as your store grows.
Add Cart Functionality and Handle Checkout
Build a cart context using React's Context API to manage cart state across your site. Implement functions for adding items, updating quantities, and removing products.
1// context/CartContext.tsx
2import React, { createContext, useState, useContext, useEffect } from 'react';
3import { Product } from '../types/product';
4
5type CartItem = {
6 id: number;
7 product: Product;
8 quantity: number;
9};
10
11type CartContextType = {
12 items: CartItem[];
13 addItem: (product: Product, quantity: number) => void;
14 updateQuantity: (itemId: number, quantity: number) => void;
15 removeItem: (itemId: number) => void;
16 clearCart: () => void;
17 totalItems: number;
18 subtotal: number;
19};
20
21const CartContext = createContext<CartContextType | undefined>(undefined);
22
23export function CartProvider({ children }) {
24 const [items, setItems] = useState<CartItem[]>([]);
25
26 // Load cart from localStorage on initial render
27 useEffect(() => {
28 const savedCart = localStorage.getItem('cart');
29 if (savedCart) {
30 setItems(JSON.parse(savedCart));
31 }
32 }, []);
33
34 // Save cart to localStorage whenever it changes
35 useEffect(() => {
36 localStorage.setItem('cart', JSON.stringify(items));
37 }, [items]);
38
39 const addItem = (product: Product, quantity: number) => {
40 setItems(prevItems => {
41 const existingItem = prevItems.find(item => item.product.id === product.id);
42
43 if (existingItem) {
44 // Update quantity if item already exists
45 return prevItems.map(item =>
46 item.id === existingItem.id
47 ? { ...item, quantity: item.quantity + quantity }
48 : item
49 );
50 } else {
51 // Add new item
52 return [...prevItems, {
53 id: Date.now(),
54 product,
55 quantity
56 }];
57 }
58 });
59 };
60
61 const updateQuantity = (itemId: number, quantity: number) => {
62 setItems(prevItems =>
63 prevItems.map(item =>
64 item.id === itemId ? { ...item, quantity } : item
65 )
66 );
67 };
68
69 const removeItem = (itemId: number) => {
70 setItems(prevItems => prevItems.filter(item => item.id !== itemId));
71 };
72
73 const clearCart = () => {
74 setItems([]);
75 };
76
77 const totalItems = items.reduce((total, item) => total + item.quantity, 0);
78
79 const subtotal = items.reduce(
80 (total, item) => total + item.product.price * item.quantity,
81 0
82 );
83
84 return (
85 <CartContext.Provider value={{
86 items,
87 addItem,
88 updateQuantity,
89 removeItem,
90 clearCart,
91 totalItems,
92 subtotal
93 }}>
94 {children}
95 </CartContext.Provider>
96 );
97}
98
99// Custom hook to use the cart context
100export function useCart() {
101 const context = useContext(CartContext);
102 if (context === undefined) {
103 throw new Error('useCart must be used within a CartProvider');
104 }
105 return context;
106}
Create a cart sidebar component showing items and totals with an intuitive interface. Design a secure checkout flow by sending cart data to your backend API. Always validate on the server. Never trust cart totals or pricing from the frontend. Recalculate everything on your backend before processing payments, with your Strapi API verifying prices and availability.
Optimize for Performance, SEO, and Core Web Vitals
The Next.js Image component automatically optimizes and makes images responsive. Use code splitting to load features only when needed, keeping your initial page load fast.
Add product structured data to boost search visibility with proper schema markup for product details, pricing, and availability:
1// components/ProductSchema.tsx
2import Head from 'next/head';
3import { Product } from '../types/product';
4
5type Props = {
6 product: Product;
7 siteUrl: string;
8};
9
10export default function ProductSchema({ product, siteUrl }: Props) {
11 const { name, description, price, images, sku, inStock } = product;
12
13 const schema = {
14 "@context": "https://schema.org/",
15 "@type": "Product",
16 "name": name,
17 "description": description,
18 "image": images.data.map(img => `${siteUrl}${img.url}`),
19 "sku": sku,
20 "mpn": sku,
21 "offers": {
22 "@type": "Offer",
23 "url": `${siteUrl}/products/${product.slug}`,
24 "priceCurrency": "USD",
25 "price": price,
26 "availability": inStock
27 ? "https://schema.org/InStock"
28 : "https://schema.org/OutOfStock"
29 }
30 };
31
32 return (
33 <Head>
34 <script
35 type="application/ld+json"
36 dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
37 />
38 </Head>
39 );
40}
You can improve Core Web Vitals by reducing layout shifts with proper image dimensions, speeding up Largest Contentful Paint by prioritizing above-the-fold content, and enhancing interactivity by deferring non-critical JavaScript. Generate dynamic sitemaps that automatically include all product pages with appropriate metadata.
Deploy Your Store to Production
For your Strapi backend, consider platforms like Render or Strapi Cloud that offer simple deployment with managed databases. Set up all environment variables, including database credentials and API keys. For the frontend, Vercel provides the smoothest experience with automatic builds and preview deployments.
1// vercel.json
2{
3 "version": 2,
4 "builds": [
5 {
6 "src": "package.json",
7 "use": "@vercel/next"
8 }
9 ],
10 "env": {
11 "NEXT_PUBLIC_STRAPI_API_URL": "https://your-strapi-backend.com/api",
12 "NEXT_PUBLIC_SITE_URL": "https://your-store-domain.com",
13 "STRIPE_SECRET_KEY": "@stripe-secret-key",
14 "STRIPE_WEBHOOK_SECRET": "@stripe-webhook-secret"
15 },
16 "images": {
17 "domains": ["your-strapi-backend.com"]
18 }
19}
Enable build caching for faster deployments and configure Next.js for optimal production performance. Your pre-launch checklist should include verifying environment variables, testing API endpoints, setting up SSL certificates, configuring monitoring, enabling CDN for assets, setting up database backups, and testing the complete purchase flow, including webhooks.
Consider a CI/CD pipeline that runs tests and deploys to staging before production. This keeps your store stable as you add features and content.
Build a Store That's Secure, Monitorable, and Ready to Scale
Security is crucial. Enable HTTPS for both the Strapi admin panel and Next.js frontend. Set strict CORS policies to allow requests only from your domains, and secure JWTs by storing them in HttpOnly cookies to prevent XSS attacks. Rotate secrets regularly and set short expiration times for tokens to ensure optimal security.
Strapi's role-based access control (RBAC) ensures admin users only have the necessary permissions, which you can manage in the Strapi Admin Panel. It also integrates with error tracking tools like Sentry, allowing you to catch issues early. The Strapi Sentry plugin sends errors to Sentry and attaches metadata, streamlining error management.
Scaling is simple with Strapi’s decoupled architecture. You can independently scale components by adding more Strapi instances behind a load balancer, optimizing your database, and using Redis caching. A CDN for static assets boosts global performance. Monitor metrics like slow API responses and increasing error rates to identify scaling needs early.
Focus on specific bottlenecks: scale the backend for frequent updates or optimize the frontend for high traffic. This targeted approach ensures smooth growth while keeping costs in check. Try Strapi v5 and Strapi Cloud to scale securely and efficiently.