DEV Community

Cover image for No database, no problem: e-commerce with Nuxt Content and Stripe
Franck LEBAS
Franck LEBAS

Posted on

No database, no problem: e-commerce with Nuxt Content and Stripe

I've been building frontends for a while now, and one thing that still surprises me is how much infrastructure we accept as a given for small e-commerce projects. A database. An admin panel. A CMS subscription. A backend to glue it all together.

For a curated catalog of 10 to 50 products, that's a lot of moving parts.

I wanted to see how far I could go in the other direction. The result is AURORA Commerce — a Nuxt 4 storefront where products live in YAML files, payments go through Stripe, and the infrastructure cost is zero.

Here's how I built it and why it might be the right approach for your next project.


The idea: your repo is your database

Instead of querying a database or calling a CMS API, product data lives directly in the repository:

content/
  products/
    heavyweight-crewneck-charcoal.yml
    linen-midi-dress-terracotta.yml
    oxford-button-down-shirt-white.yml
Enter fullscreen mode Exit fullscreen mode

Each file is a complete product record:

productId: 6
title: "Heavyweight Crewneck - Charcoal"
titleFr: Sweat Crewneck Heavyweight - Charbon
slug: heavyweight-crewneck-charcoal
price: 109
category: sweats
badge: Core line
highlight: Brushed cotton 420 g/m2
description: "Oversize sweatshirt in ultra-soft brushed cotton. 420 g/m2 weight,"
  reinforced collar, and premium finishes. The staple you never take off.
descriptionFr: Sweat oversize en coton brosse ultra-doux 420 g/m2. Col renforce,
  finitions soignees. Le basique premium que tu portes tous les jours.
images:
  - https://your-cdn.com/product-1.jpg
  - https://your-cdn.com/product-2.jpg
  - https://your-cdn.com/product-3.jpg
sizes: [S, M, L, XL]
fabricWeightGsm: 420
origin: Knit in France, made in Portugal
sizeChart:
  - { size: S, chestCm: 90, waistCm: 74, lengthCm: 67 }
  - { size: M, chestCm: 95, waistCm: 79, lengthCm: 68 }
  - { size: L, chestCm: 100, waistCm: 84, lengthCm: 69 }
  - { size: XL, chestCm: 106, waistCm: 90, lengthCm: 70 }
reviews:
  - { author: Theo G., city: Nantes, rating: 5, date: '2026-03-07',
      quote: The weight is perfect and the finish feels way above standard sweatshirts. }
Enter fullscreen mode Exit fullscreen mode

Add a product by duplicating a file. Update a price by changing a number. Deploy on push. No dashboard, no migration, no API key to rotate.

It sounds almost too simple. It kind of is — and that's the point.


Querying with Nuxt Content v3

Nuxt Content v3 handles the YAML parsing and exposes a typed query API. Fetching the full catalog:

// pages/boutique.vue
const { data: products } = await useAsyncData('products', () =>
  queryCollection('products')
    .where('active', '=', true)
    .order('productId', 'ASC')
    .all()
)
Enter fullscreen mode Exit fullscreen mode

A single product by slug:

// pages/produit/[slug].vue
const { data: product } = await useAsyncData(`product-${slug}`, () =>
  queryCollection('products')
    .where('slug', '=', slug)
    .first()
)
Enter fullscreen mode Exit fullscreen mode

Nuxt Content generates a typed collection from your YAML schema automatically. You get autocomplete on product.sizeChart, product.titleFr, product.fabricWeightGsm — the whole thing. TypeScript is happy. You are happy.


The Stripe integration

The checkout flow is straightforward:

  1. User builds a cart (Pinia)
  2. Frontend calls a server route with the cart items
  3. Server creates a Stripe Checkout session and returns the URL
  4. Frontend redirects

The server route is the only backend code in the project:

// server/api/checkout-session.post.ts
import Stripe from 'stripe'

export default defineEventHandler(async (event) => {
  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
  const body = await readBody(event)

  const session = await stripe.checkout.sessions.create({
    mode: 'payment',
    line_items: body.items.map((item: CartItem) => ({
      quantity: item.quantity,
      price_data: {
        currency: 'eur',
        unit_amount: item.price * 100, // YAML stores euros, Stripe wants cents
        product_data: {
          name: item.title,
          images: [item.thumbnail],
        },
      },
    })),
    success_url: `${process.env.NUXT_PUBLIC_APP_URL}/success`,
    cancel_url: `${process.env.NUXT_PUBLIC_APP_URL}/panier`,
    shipping_address_collection: {
      allowed_countries: ['FR', 'BE', 'CH', 'DE', 'GB', 'SE'],
    },
  })

  return { url: session.url }
})
Enter fullscreen mode Exit fullscreen mode

One detail worth noting: I use price_data instead of a Stripe price ID. This means products don't need to exist in the Stripe dashboard at all — the source of truth stays in the YAML files. One less thing to keep in sync.


Bilingual without a translation library

The storefront supports English and French natively. No i18n library, no translation files. The strategy is simple: bilingual fields directly in the YAML (title / titleFr, description / descriptionFr), combined with Nuxt's built-in routing strategy (prefix_except_default):

/boutique     → English
/fr/boutique  → French
Enter fullscreen mode Exit fullscreen mode

A small composable resolves the right field based on the active locale:

// composables/useLocaleField.ts
export function useLocaleField() {
  const { locale } = useI18n()

  function t(en: string, fr?: string): string {
    return locale.value === 'fr' && fr ? fr : en
  }

  return { t }
}
Enter fullscreen mode Exit fullscreen mode

Usage in templates:

<h1>{{ t(product.title, product.titleFr) }}</h1>
<p>{{ t(product.description, product.descriptionFr) }}</p>
Enter fullscreen mode Exit fullscreen mode

The copy lives with the product. No keys to manage, no files to synchronize.


Design tokens in one place

The entire visual identity is controlled from tailwind.config.ts:

theme: {
  extend: {
    colors: {
      brand: {
        primary: '#0A0A0A',
        accent: '#C9A96E',
        surface: '#F8F6F2',
      },
    },
    fontFamily: {
      display: ['Cormorant Garamond', 'serif'],
      body: ['DM Sans', 'sans-serif'],
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

Change brand.accent once and every button, badge, and highlight updates across the storefront. No component hunting.


When this makes sense — and when it doesn't

This approach works well when:

  • Your catalog is small and curated (under ~200 products)
  • You want zero ongoing infrastructure cost
  • You're comfortable with a git-based workflow for content updates
  • You want full ownership — no platform lock-in

It's not the right fit if you need real-time inventory management, a non-technical client who needs a visual CMS, or complex product variants across multiple axes.

For the CMS case specifically: the architecture supports swapping queryCollection for a Sanity or Contentful client behind a shared adapter interface. The rest of the storefront doesn't need to change.


The result

A full storefront — home, shop, product detail, cart, Stripe checkout, success and cancel pages, bilingual, dark mode, SEO-ready — deployable on Vercel in under an hour.

Live demo
AURORA Commerce on Gumroad — Starter €29 / Pro €59

If you have questions or want to discuss the architecture, drop a comment. Happy to talk about it.


Built with Nuxt 4, Vue 3, Nuxt Content v3, Pinia, Tailwind CSS, Stripe, and Bun.

Top comments (0)