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
npm install @madebylars.com/mbl-shop @madebylars.com/mbl-paymentWhat 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
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()
<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()
<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()
<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()
// 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
// 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()
})