Nuxt Module
mbl-shop · v1.0.0 npm

E-commerce without the overhead.

mbl-shop handles cart state, product catalog, locale-aware pricing, and checkout flow. All payment processing is delegated to mbl-payment — no provider SDKs in here.

Install

terminal
npm install @madebylars.com/mbl-shop @madebylars.com/mbl-payment

What you get

🛒

Reactive cart

SSR-safe via useState. Persisted automatically to localStorage, cookie, or in-memory. State is shared across all components.

📦

Provider-agnostic products

Pass products in directly or fetch from any endpoint. The built-in GET /api/shop/products stub is overridable from the host app.

💳

Checkout delegation

useShopCheckout() bridges the cart and mbl-payment — maps cart items to Stripe line items and redirects. No payment SDK needed here.

💰

Locale-aware pricing

formatPrice() is auto-imported everywhere. Currency and locale come from module config and can be overridden per-call.

🏷️

One-time & subscriptions

Products declare type: "one_time" | "subscription". Subscription products carry interval and intervalCount for display and billing.

🔗

Works with mbl-payment

Designed as a companion to mbl-payment. mbl-shop owns the cart and catalog; mbl-payment owns all provider SDKs and API routes.

Configuration

nuxt.config.ts
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@madebylars.com/mbl-shop', '@madebylars.com/mbl-payment'],

  shop: {
    currency: 'USD',
    locale: 'en-US',
    paymentsProvider: 'stripe',
    cart: {
      persistence: 'localStorage', // 'localStorage' | 'cookie' | 'none'
      maxItems: 100,
    },
    routes: {
      shop: '/shop',
      cart: '/shop/cart',
      checkout: '/shop/checkout',
      success: '/shop/success',
      cancel: '/shop/cancel',
    },
  },
})

useProducts()

pages/shop/index.vue
<script setup>
const { products, loading, fetchProducts, setProducts } = useProducts()

// Fetch from the built-in stub (override in server/api/shop/products.get.ts)
await fetchProducts()

// Or set products directly (from a CMS, DB, etc.)
setProducts([
  { id: '1', name: 'T-Shirt', price: 29.99, type: 'one_time', priceId: 'price_abc123' },
  { id: '2', name: 'Monthly Plan', price: 9.99, type: 'subscription', interval: 'month', priceId: 'price_def456' },
])
</script>

<template>
  <div v-for="product in products" :key="product.id">
    {{ product.name }} — {{ formatPrice(product.price) }}
  </div>
</template>

useCart()

pages/shop/cart.vue
<script setup>
const { items, itemCount, subtotal, isEmpty, addItem, removeItem, updateQuantity, clearCart } = useCart()

// addItem resolves the price at call time and persists to localStorage
addItem(product, 2)
</script>

<template>
  <p v-if="isEmpty">Your cart is empty.</p>
  <ul v-else>
    <li v-for="item in items" :key="item.productId">
      {{ item.product.name }} × {{ item.quantity }}
      — {{ formatPrice(item.price * item.quantity) }}
      <button @click="removeItem(item.productId)">Remove</button>
    </li>
  </ul>
  <p>Subtotal: {{ formatPrice(subtotal) }}</p>
</template>

useShopCheckout()

pages/shop/cart.vue
<script setup>
// Bridges the cart and mbl-payment — maps cart items to Stripe line items
// and redirects the browser to Stripe Checkout.
const { startCheckout, loading, error } = useShopCheckout()
</script>

<template>
  <p v-if="error" style="color: red">{{ error }}</p>
  <button :disabled="loading" @click="startCheckout()">
    {{ loading ? 'Redirecting…' : 'Checkout' }}
  </button>
</template>

formatPrice()

anywhere in your app
// Auto-imported everywhere — uses module config currency + locale
formatPrice(29.99)         // → '$29.99'
formatPrice(9.99, { currency: 'EUR', locale: 'de-DE' }) // → '9,99 €'

Custom products endpoint

The module registers a stub GET /api/shop/products that returns an empty array. Override it by creating the same route in your app — Nuxt gives host-app routes priority.

server/api/shop/products.get.ts
// server/api/shop/products.get.ts
// Host-app routes take priority over the module stub — just create this file.
export default defineEventHandler(async () => {
  // fetch from your database, CMS, etc.
  return await db.products.findAll()
})