[{"data":1,"prerenderedAt":18},["ShallowReactive",2],{"blog-article-no-database-no-problem-e-commerce-with-nuxt-content-and-stripe-cbc-fr":3},{"slug":4,"title":5,"description":6,"tags":7,"published_at":12,"reading_time_minutes":13,"cover_image":14,"canonical_url":15,"html":16,"cached":17},"no-database-no-problem-e-commerce-with-nuxt-content-and-stripe-cbc","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...",[8,9,10,11],"nuxt","tailwindcss","stripe","bunjs","2026-03-31T14:47:19Z",4,"https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1000,height=420,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5cjuf2i3drcet06mj5zh.png","https:\u002F\u002Fdev.to\u002Ffrancklebas\u002Fno-database-no-problem-e-commerce-with-nuxt-content-and-stripe-cbc","\u003Cp>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.\u003C\u002Fp>\n\u003Cp>For a curated catalog of 10 to 50 products, that’s a lot of moving parts.\u003C\u002Fp>\n\u003Cp>I wanted to see how far I could go in the other direction. The result is \u003Cstrong>AURORA Commerce\u003C\u002Fstrong> — a Nuxt 4 storefront where products live in YAML files, payments go through Stripe, and the infrastructure cost is zero.\u003C\u002Fp>\n\u003Cp>Here’s how I built it and why it might be the right approach for your next project.\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>The idea: your repo is your database\u003C\u002Fh2>\n\u003Cp>Instead of querying a database or calling a CMS API, product data lives directly in the repository:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-plaintext\">content\u002F\n  products\u002F\n    heavyweight-crewneck-charcoal.yml\n    linen-midi-dress-terracotta.yml\n    oxford-button-down-shirt-white.yml\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Each file is a complete product record:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yaml\">productId: 6\ntitle: Heavyweight Crewneck - Charcoal\ntitleFr: Sweat Crewneck Heavyweight - Charbon\nslug: heavyweight-crewneck-charcoal\nprice: 109\ncategory: sweats\nbadge: Core line\nhighlight: Brushed cotton 420 g\u002Fm2\ndescription: Oversize sweatshirt in ultra-soft brushed cotton. 420 g\u002Fm2 weight,\n  reinforced collar, and premium finishes. The staple you never take off.\ndescriptionFr: Sweat oversize en coton brosse ultra-doux 420 g\u002Fm2. Col renforce,\n  finitions soignees. Le basique premium que tu portes tous les jours.\nimages:\n  - https:\u002F\u002Fyour-cdn.com\u002Fproduct-1.jpg\n  - https:\u002F\u002Fyour-cdn.com\u002Fproduct-2.jpg\n  - https:\u002F\u002Fyour-cdn.com\u002Fproduct-3.jpg\nsizes: [S, M, L, XL]\nfabricWeightGsm: 420\norigin: Knit in France, made in Portugal\nsizeChart:\n  - { size: S, chestCm: 90, waistCm: 74, lengthCm: 67 }\n  - { size: M, chestCm: 95, waistCm: 79, lengthCm: 68 }\n  - { size: L, chestCm: 100, waistCm: 84, lengthCm: 69 }\n  - { size: XL, chestCm: 106, waistCm: 90, lengthCm: 70 }\nreviews:\n  - { author: Theo G., city: Nantes, rating: 5, date: '2026-03-07',\n      quote: The weight is perfect and the finish feels way above standard sweatshirts. }\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>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.\u003C\u002Fp>\n\u003Cp>It sounds almost too simple. It kind of is — and that’s the point.\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>Querying with Nuxt Content v3\u003C\u002Fh2>\n\u003Cp>\u003Ca href=\"https:\u002F\u002Fcontent.nuxt.com\u002F\">Nuxt Content v3\u003C\u002Fa> handles the YAML parsing and exposes a typed query API. Fetching the full catalog:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-typescript\">\u002F\u002F pages\u002Fboutique.vue\nconst { data: products } = await useAsyncData('products', () =&gt;\n  queryCollection('products')\n    .where('active', '=', true)\n    .order('productId', 'ASC')\n    .all()\n)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>A single product by slug:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-typescript\">\u002F\u002F pages\u002Fproduit\u002F[slug].vue\nconst { data: product } = await useAsyncData(`product-${slug}`, () =&gt;\n  queryCollection('products')\n    .where('slug', '=', slug)\n    .first()\n)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Nuxt Content generates a typed collection from your YAML schema automatically. You get autocomplete on \u003Ccode>product.sizeChart\u003C\u002Fcode>, \u003Ccode>product.titleFr\u003C\u002Fcode>, \u003Ccode>product.fabricWeightGsm\u003C\u002Fcode> — the whole thing. TypeScript is happy. You are happy.\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>The Stripe integration\u003C\u002Fh2>\n\u003Cp>The checkout flow is straightforward:\u003C\u002Fp>\n\u003Col>\n\u003Cli>User builds a cart (Pinia)\u003C\u002Fli>\n\u003Cli>Frontend calls a server route with the cart items\u003C\u002Fli>\n\u003Cli>Server creates a Stripe Checkout session and returns the URL\u003C\u002Fli>\n\u003Cli>Frontend redirects\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>The server route is the only backend code in the project:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-typescript\">\u002F\u002F server\u002Fapi\u002Fcheckout-session.post.ts\nimport Stripe from 'stripe'\n \nexport default defineEventHandler(async (event) =&gt; {\n  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)\n  const body = await readBody(event)\n \n  const session = await stripe.checkout.sessions.create({\n    mode: 'payment',\n    line_items: body.items.map((item: CartItem) =&gt; ({\n      quantity: item.quantity,\n      price_data: {\n        currency: 'eur',\n        unit_amount: item.price * 100, \u002F\u002F YAML stores euros, Stripe wants cents\n        product_data: {\n          name: item.title,\n          images: [item.thumbnail],\n        },\n      },\n    })),\n    success_url: `${process.env.NUXT_PUBLIC_APP_URL}\u002Fsuccess`,\n    cancel_url: `${process.env.NUXT_PUBLIC_APP_URL}\u002Fpanier`,\n    shipping_address_collection: {\n      allowed_countries: ['FR', 'BE', 'CH', 'DE', 'GB', 'SE'],\n    },\n  })\n \n  return { url: session.url }\n})\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>One detail worth noting: I use \u003Ccode>price_data\u003C\u002Fcode> 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.\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>Bilingual without a translation library\u003C\u002Fh2>\n\u003Cp>The storefront supports English and French natively. No i18n library, no translation files. The strategy is simple: bilingual fields directly in the YAML (\u003Ccode>title\u003C\u002Fcode> \u002F \u003Ccode>titleFr\u003C\u002Fcode>, \u003Ccode>description\u003C\u002Fcode> \u002F \u003Ccode>descriptionFr\u003C\u002Fcode>), combined with Nuxt’s built-in routing strategy (\u003Ccode>prefix_except_default\u003C\u002Fcode>):\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-plaintext\">\u002Fboutique     → English\n\u002Ffr\u002Fboutique  → French\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>A small composable resolves the right field based on the active locale:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-typescript\">\u002F\u002F composables\u002FuseLocaleField.ts\nexport function useLocaleField() {\n  const { locale } = useI18n()\n \n  function t(en: string, fr?: string): string {\n    return locale.value === 'fr' &amp;&amp; fr ? fr : en\n  }\n \n  return { t }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Usage in templates:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-vue\">&lt;h1&gt;{{ t(product.title, product.titleFr) }}&lt;\u002Fh1&gt;\n&lt;p&gt;{{ t(product.description, product.descriptionFr) }}&lt;\u002Fp&gt;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The copy lives with the product. No keys to manage, no files to synchronize.\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>Design tokens in one place\u003C\u002Fh2>\n\u003Cp>The entire visual identity is controlled from \u003Ccode>tailwind.config.ts\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-typescript\">theme: {\n  extend: {\n    colors: {\n      brand: {\n        primary: '#0A0A0A',\n        accent: '#C9A96E',\n        surface: '#F8F6F2',\n      },\n    },\n    fontFamily: {\n      display: ['Cormorant Garamond', 'serif'],\n      body: ['DM Sans', 'sans-serif'],\n    },\n  },\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Change \u003Ccode>brand.accent\u003C\u002Fcode> once and every button, badge, and highlight updates across the storefront. No component hunting.\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>When this makes sense — and when it doesn’t\u003C\u002Fh2>\n\u003Cp>This approach works well when:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Your catalog is \u003Cstrong>small and curated\u003C\u002Fstrong> (under ~200 products)\u003C\u002Fli>\n\u003Cli>You want \u003Cstrong>zero ongoing infrastructure cost\u003C\u002Fstrong>\u003C\u002Fli>\n\u003Cli>You’re comfortable with a \u003Cstrong>git-based workflow\u003C\u002Fstrong> for content updates\u003C\u002Fli>\n\u003Cli>You want \u003Cstrong>full ownership\u003C\u002Fstrong> — no platform lock-in\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>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.\u003C\u002Fp>\n\u003Cp>For the CMS case specifically: the architecture supports swapping \u003Ccode>queryCollection\u003C\u002Fcode> for a Sanity or Contentful client behind a shared adapter interface. The rest of the storefront doesn’t need to change.\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>The result\u003C\u002Fh2>\n\u003Cp>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.\u003C\u002Fp>\n\u003Cp>→ \u003Ca href=\"https:\u002F\u002Fnuxt-commerce-flame-chi.vercel.app\u002F\">Live demo\u003C\u002Fa>\n→ \u003Ca href=\"https:\u002F\u002Ffrancklebas.gumroad.com\u002Fl\u002Fnuxt-aurora\">AURORA Commerce on Gumroad\u003C\u002Fa> — Starter €29 \u002F Pro €59\u003C\u002Fp>\n\u003Cp>If you have questions or want to discuss the architecture, drop a comment. Happy to talk about it.\u003C\u002Fp>\n\u003Chr>\n\u003Cp>\u003Cem>Built with Nuxt 4, Vue 3, Nuxt Content v3, Pinia, Tailwind CSS, Stripe, and Bun.\u003C\u002Fem>\u003C\u002Fp>\n",false,1775820116899]