nuxt-cart
v1.1.3
Published
A generic, reusable shopping cart module for Nuxt 4 with localStorage persistence, coupon support, and optional Nuxt UI components
Downloads
899
Maintainers
Readme
Nuxt Cart
A generic, reusable shopping cart module for Nuxt 4 applications. Zero-config localStorage cart out of the box, opt-in server API routes, coupon support, and a hook-based payment gateway system.
Features
🛒 Cart Management
- Add, remove, and update items with quantity support
- Items grouped by
productIdwith automatic quantity merging - Configurable max quantity per item
💾 Persistence
- Automatic localStorage persistence with hydration
- SSR-safe — no localStorage access on the server
- Type-guard validation on load to prevent corrupt data
- Auto-save on change with
beforeunloadfallback
📐 Computed Values
totalAmount— sum ofprice × quantitydiscountedTotal— total after coupon discountitemCount— sum of all quantitiesisEmpty— quick empty check
🎫 Coupon Support
- Built-in
applyCoupon/removeCouponAPI discountedTotalcomputed for fixed and percentage discounts- Hook-based validation via
onValidateCoupon - Persisted with cart state
- Built-in
🧩 Built on Pinia
- Pinia setup store under the hood
- Devtools support out of the box
- Shared state across components
🎨 Nuxt UI Components
NCartDrawer— slide-out cart drawer withUSlideoverNCartItem— line item with image, price, quantity controlNCartSummary— subtotal, discount, grand total, checkout CTANCartQuantity—+/-quantity selector
🔌 Server API Routes
- 9 REST endpoints for cart CRUD, coupons, and checkout
- Token-based cart identification with httpOnly cookies
- Database-backed persistence via db0 (SQLite, MySQL, PostgreSQL)
- Automatic client-server sync with optimistic local updates
🔌 Pluggable Architecture
- Hook-based payment gateway integration via
onCheckout/checkout - Custom coupon validation via
onValidateCoupon
- Hook-based payment gateway integration via
Installation
npm install nuxt-cartThe module requires @pinia/nuxt in your project:
npm install @pinia/nuxtIf you plan to use server API routes with a database, also install the driver for your database:
# SQLite (default)
npm install better-sqlite3
# MySQL
npm install mysql2
# PostgreSQL
npm install pgThe database drivers are optional peer dependencies — they are never installed or loaded unless you explicitly add them. The module works as a client-only localStorage cart with zero extra packages.
Add the module to your nuxt.config.ts:
export default defineNuxtConfig({
modules: ['nuxt-cart'],
nuxtCart: {
persist: true,
storageKey: 'my-cart',
currency: 'USD',
maxQuantity: 99,
apiRoutes: true, // Enable REST API + DB persistence
connector: { // Optional (default: SQLite)
name: 'sqlite',
options: { path: './data/cart.sqlite3' }
}
},
})Note:
@pinia/nuxtis declared as a module dependency and is auto-registered by Nuxt. You still need to install it (npm install @pinia/nuxt), but you do not need to list it manually inmodulesunless you want to pass Pinia-specific options.
For UI components, also add @nuxt/ui to modules:
export default defineNuxtConfig({
modules: ['nuxt-cart', '@nuxt/ui'],
})Zero-Config Quick Start
The module works with no configuration at all:
export default defineNuxtConfig({
modules: ['nuxt-cart'],
})Default settings:
- localStorage persistence enabled
- Storage key:
nuxt-cart - Max quantity per item: 99
- Currency:
USD - Coupons disabled
- API routes disabled
- Database: not configured by default (install
better-sqlite3and setapiRoutes: trueto enable)
Usage
Basic (add to cart, display)
<script setup lang="ts">
const cart = useCart()
function buy(product: { id: string; name: string; price: number }) {
cart.addItem({
productId: product.id,
name: product.name,
price: product.price,
})
}
</script>
<template>
<button @click="buy(product)">Add to Cart</button>
<p v-if="!cart.isEmpty.value">
{{ cart.itemCount.value }} items — {{ cart.totalAmount.value }}€
</p>
</template>With components
Requires @nuxt/ui v4:
<script setup lang="ts">
const cart = useCart()
const isCartOpen = ref(false)
</script>
<template>
<UButton @click="isCartOpen = true">
Cart ({{ cart.itemCount.value }})
</UButton>
<NCartDrawer
:open="isCartOpen"
@close="isCartOpen = false"
@checkout="handleCheckout"
/>
</template>With coupons
const cart = useCart()
// Register a validation hook
cart.onValidateCoupon(async (code) => {
const { data } = await useFetch('/api/coupon/validate', { query: { code } })
return data.value // { code, discount, type } or null
})
// Apply a coupon
await cart.applyCoupon('SUMMER20')
console.log(cart.discountedTotal.value) // total after discount
// Remove coupon
cart.removeCoupon()With quantity
const cart = useCart()
// Add with specific quantity (default: 1)
cart.addItem({ productId: 'p1', name: 'Product', price: 100, quantity: 3 })
// Updating quantity
cart.updateQuantity('p1', 5)Removing items
cart.removeItem('p1') // removes entire product line
cart.updateQuantity('p1', 0) // also removesChecking cart state
const cart = useCart()
cart.items // CartItem[]
cart.coupon // Coupon | null
cart.totalAmount // sum of price * quantity
cart.discountedTotal // total after coupon discount
cart.itemCount // sum of all quantities
cart.isEmpty // boolean
cart.isHydrated // true after localStorage restoreWith server API routes
Prerequisite: You must install the database driver package first — see Installation above.
When apiRoutes: true, all mutations sync to the server, and the cart is restored from the server on hydration:
const cart = useCart()
// Token is auto-managed via httpOnly cookie
cart.addItem({ productId: 'p1', name: 'Product', price: 100 })
// → optimistic local update + POST /api/cart/items
// Server status
cart.isServerSynced // true after successful server sync
cart.cartToken // cart token from cookieThe REST API uses a db0 database with auto-migration on first run.
With checkout hooks
const cart = useCart()
// Register a payment handler
cart.onCheckout(async (cartData) => {
const { data } = await useFetch('/api/checkout', {
method: 'POST',
body: cartData,
})
if (data.value?.url) return { redirectUrl: data.value.url }
return { error: 'Checkout failed' }
})
// Trigger checkout — runs all registered hooks
const result = await cart.checkout()
if (result.redirectUrl) window.location.href = result.redirectUrlMultiple checkout hooks can be registered; they run sequentially. The first to return a redirectUrl or error wins.
API
useCart() composable
| Return | Type | Description |
|--------|------|-------------|
| items | Ref<CartItem[]> | Array of cart items |
| coupon | Ref<Coupon \| null> | Currently applied coupon |
| totalAmount | ComputedRef<number> | Sum of price × quantity (before discount) |
| discountedTotal | ComputedRef<number> | Total after coupon discount |
| itemCount | ComputedRef<number> | Total number of items (sum of quantities) |
| isEmpty | ComputedRef<boolean> | Whether the cart has any items |
| isHydrated | Ref<boolean> | true after localStorage data is loaded |
| cartToken | Ref<string \| null> | Server cart token (when apiRoutes: true) |
| isServerSynced | Ref<boolean> | true after server sync completes |
| addItem(input) | () => void | Add item (merges by productId, increments quantity) |
| removeItem(productId) | (id: string) => void | Remove all of a product line |
| updateQuantity(productId, qty) | (id: string, qty: number) => void | Set exact quantity (removes if 0) |
| clear() | () => void | Empty the cart and remove coupon |
| applyCoupon(code) | (code: string) => Promise<void> | Apply coupon via registered hook |
| removeCoupon() | () => void | Remove the active coupon |
| onValidateCoupon(hook) | (hook: ValidateCouponHook) => void | Register coupon validation handler |
| onCheckout(hook) | (hook: CheckoutHook) => void | Register checkout handler |
| checkout() | () => Promise<Result> | Execute all checkout hooks, returns { redirectUrl?, error? } |
| persist() | () => void | Save to localStorage |
| load() | () => void | Restore from localStorage |
addItem input
interface AddItemInput {
productId: string
name: string
price: number
quantity?: number // defaults to 1
image?: string
metadata?: Record<string, unknown>
}If an item with the same productId already exists, its quantity is incremented (clamped to maxQuantity).
Configuration
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| persist | boolean | true | Enable localStorage persistence |
| storageKey | string | 'nuxt-cart' | localStorage key |
| currency | string | 'USD' | Currency format |
| maxQuantity | number | 99 | Maximum quantity per item |
| coupons | boolean | false | Enable coupon support |
| apiRoutes | boolean | false | Enable server API routes + DB persistence |
| connector | DatabaseConfig | { name: 'sqlite', options: { path: './data/cart.sqlite3' } } | Database connector config (db0). Requires the corresponding driver package to be installed (better-sqlite3, mysql2, or pg). |
Types
Import types from the module:
import type { ModuleOptions, CartItem, CartState, Coupon, CheckoutHook, ValidateCouponHook, DatabaseConfig, DatabaseType } from 'nuxt-cart'Persistence Behavior
When persist: true (default), the client-only plugin automatically:
- On client mount — loads cart from
localStorageand validates item/coupon shapes - On every change — deep-watches
items+couponand auto-saves - On
beforeunload/pagehide— saves as a safety net
On the server, no localStorage access occurs and isHydrated stays false.
Set persist: false to disable automatic hydration and auto-save. Manual cart.persist() / cart.load() remain available.
Corrupt data is silently discarded; invalid items and coupons are filtered out during hydration.
Server DB Persistence (when apiRoutes: true)
Prerequisite: Install the database driver package (
pnpm add better-sqlite3,mysql2, orpg) — the module does not install it automatically.
In addition to localStorage, the cart state is persisted to a database via db0:
- Cart created via
POST /api/cartreturns a token (stored in httpOnly cookie) - All mutations sync to the server (optimistic local + fire-and-forget)
- On hydration, fetches server cart state as the source of truth
- Auto-migration creates tables on first run
Agent Skill
Install the nuxt-cart Agent Skill so your AI coding agent (Cursor, Claude Code, etc.) has procedural knowledge for this module. One-time install:
npx skills add rrd108/nuxt-cartModule Structure
nuxt-cart/
├── src/
│ ├── module.ts # Module entry point
│ ├── types.ts # TypeScript types
│ ├── default-options.ts # Default configuration
│ └── runtime/
│ ├── plugin.ts # Hydration + auto-persist
│ ├── composables/
│ │ └── useCart.ts # Pinia store + composable with server sync
│ ├── components/ # Registered when @nuxt/ui is in modules
│ │ ├── NCartDrawer.vue
│ │ ├── NCartItem.vue
│ │ ├── NCartSummary.vue
│ │ └── NCartQuantity.vue
│ └── server/ # REST API + DB (when apiRoutes: true)
│ ├── api/cart/
│ │ ├── index.get.ts
│ │ ├── index.post.ts
│ │ ├── index.delete.ts
│ │ ├── items.post.ts
│ │ ├── items/[itemId].patch.ts
│ │ ├── items/[itemId].delete.ts
│ │ ├── coupon.post.ts
│ │ ├── coupon.delete.ts
│ │ └── checkout.post.ts
│ ├── composables/useCartDb.ts
│ ├── middleware/cart-token.ts
│ ├── plugins/auto-migrate.ts
│ └── utils/
│ ├── db.ts
│ ├── migrate.ts
│ ├── create-carts-table.ts
│ ├── cart.ts
│ └── build-time.ts
├── playground/ # Development app
│ ├── nuxt.config.ts
│ ├── app.vue
│ ├── pages/
│ │ ├── index.vue
│ │ └── checkout.vue
│ └── server/
│ └── db/
│ └── migrations/
│ └── 001-create-cart.sql
├── test/
│ ├── composables/
│ │ └── useCart.spec.ts # 49 unit tests
│ └── server/
│ └── api.spec.ts # Server API tests
├── docs/ # VitePress documentation
├── package.json
├── build.config.ts
├── tsconfig.json
└── vitest.config.tsDevelopment
# Install dependencies
pnpm install
# Prepare stubs and playground types
pnpm dev:prepare
# Run the playground dev server
pnpm dev
# Run tests
pnpm test
# Watch mode
pnpm test:watch
# Build the module
pnpm prepack
# Documentation
pnpm docs:devImplementation Status
| Phase | Feature | Status |
|-------|---------|--------|
| 1 (MVP) | useCart() + Pinia store + localStorage + types + plugin | ✅ Done |
| 2 (Coupons) | applyCoupon, removeCoupon, discountedTotal, validation hooks | ✅ Done |
| 3 (Components) | NCartDrawer, NCartItem, NCartSummary, NCartQuantity | ✅ Done |
| 4 (Server) | REST API + DB + token middleware + checkout | ✅ Done |
| 5 (Polish) | onCheckout/checkout hooks, playground, CI, lint, publish | ✅ Done |
License
MIT
