Comprehensive Exercise — Core Features
In Phase 2, we learned about Pinia, Composable, Server API, Middleware, and Plugins—now it's time to tie them all together. Charlie's requirements are: the shopping cart must be fully integrated from the API through the Store to the page; Bob's admin dashboard must have access controls; and Alice's shopping experience must be seamless and uninterrupted.
1. What You'll Learn
- Implement a complete shopping cart CRUD API
- Integrated Pinia Shopping Cart Store with SSR state synchronization
- Develop custom Composables for
useCartanduseProductFilter - Configure the
authglobal middleware to protect the checkout page - End-to-end integration testing: API → Composable → Store → Page rendering
2. A True Story About a Team
(1) Pain Point: Modules operate independently but cannot be integrated
Alice's shopping cart page displays products, but the data is lost after refreshing. Bob's API is written, but the front end isn't calling it. The middleware is written but hasn't been integrated. Each module works fine on its own, but when they're put together, the system breaks down.
(2) Solutions for End-to-End Integration Testing
Fully integrate the data chain from API → Composable → Store → Page, ensuring a clear flow of data at every stage.
(3) Benefits: A Complete Shopping Experience
Alice can add items to the shopping cart → the items are retained after refreshing → proceed to the checkout page → the admin panel is protected by access controls; the entire process is seamless.
3. Collaborative Architecture of Phase 2 Core Functional Modules
graph TB
A[MegaShop Phase 2] --> B[Server API Layer]
A --> C[Composable Layer]
A --> D[State Layer - Pinia]
A --> E[Middleware Layer]
A --> F[Plugin Layer]
B --> B1[/api/products CRUD]
B --> B2[/api/cart CRUD]
B --> B3[/api/auth Login/Register]
C --> C1[useCart → Store + API]
C --> C2[useProductFilter → API]
C --> C3[usePriceFormat → Pure]
D --> D1[cartStore → items/total]
D --> D2[userStore → auth/profile]
E --> E1[auth.global.ts → Login check]
E --> E2[admin.ts → Role check]
F --> F1[logger → Debug]
F --> F2[stripe.client → Payment]
C1 --> D1
C1 --> B2
C2 --> B1
(1) Complete Data Flow Path
| Operation | Data Flow | Modules Involved |
|---|---|---|
| Browse Products | API → useFetch → Page | Server API |
| Add to Cart | API → useCart → cartStore → Page | API + Composable + Store |
| View Cart | cartStore → Page | Store |
| Checkout | cartStore → useCheckout → Stripe → API | Store + Composable + Plugin + API |
| Admin Dashboard | auth middleware → admin middleware → page | Middleware |
4. Complete Shopping Cart API
(1) ▶ Example: Complete Shopping Cart API
// server/api/cart/index.get.ts
export default defineEventHandler((event) => {
const sessionId = getCookie(event, 'session-id') || 'default'
const cart = mockCarts.find(c => c.sessionId === sessionId)
return cart || { sessionId, items: [], total: 0 }
})
Output:
// Execution Successful
// server/api/cart/add.post.ts
export default defineEventHandler(async (event) => {
const sessionId = getCookie(event, 'session-id') || 'default'
const { productId, quantity = 1 } = await readBody(event)
const product = mockProducts.find(p => p.id === productId)
if (!product) throw createError({ statusCode: 404, message: 'Product not found' })
let cart = mockCarts.find(c => c.sessionId === sessionId)
if (!cart) {
cart = { sessionId, items: [], total: 0 }
mockCarts.push(cart)
}
const existing = cart.items.find(i => i.productId === productId)
if (existing) {
existing.quantity += quantity
} else {
cart.items.push({ productId, name: product.name, price: product.price, quantity, image: product.image })
}
cart.total = cart.items.reduce((s, i) => s + i.price * i.quantity, 0)
setCookie(event, 'session-id', sessionId, { httpOnly: true, maxAge: 86400 * 30 })
return cart
})
// server/api/cart/remove.delete.ts
export default defineEventHandler(async (event) => {
const sessionId = getCookie(event, 'session-id') || 'default'
const { productId } = await readBody(event)
const cart = mockCarts.find(c => c.sessionId === sessionId)
if (!cart) throw createError({ statusCode: 404, message: 'Cart not found' })
cart.items = cart.items.filter(i => i.productId !== productId)
cart.total = cart.items.reduce((s, i) => s + i.price * i.quantity, 0)
return cart
})
5. Pinia Store + Composable Integration
(1) ▶ Example: Enhanced cartStore
// stores/cart.ts
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
const isLoading = ref(false)
const totalItems = computed(() => items.value.reduce((s, i) => s + i.quantity, 0))
const totalPrice = computed(() => items.value.reduce((s, i) => s + i.price * i.quantity, 0))
const formattedTotal = computed(() =>
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(totalPrice.value)
)
async function fetchCart() {
isLoading.value = true
try {
const cart = await $fetch('/api/cart')
items.value = cart.items || []
} finally {
isLoading.value = false
}
}
async function addItem(productId: number, quantity = 1) {
const cart = await $fetch('/api/cart/add', {
method: 'POST', body: { productId, quantity }
})
items.value = cart.items
}
async function removeItem(productId: number) {
const cart = await $fetch('/api/cart/remove', {
method: 'DELETE', body: { productId }
})
items.value = cart.items
}
function clearCart() {
items.value = []
}
return { items, isLoading, totalItems, totalPrice, formattedTotal, fetchCart, addItem, removeItem, clearCart }
})
interface CartItem { productId: number; name: string; price: number; quantity: number; image: string }
Output:
// Execution Successful
(2) ▶ Example: useCart Composable
// composables/useCart.ts
export function useCart() {
const cartStore = useCartStore()
const { $logger } = useNuxtApp()
const { notify } = useNotification()
async function addToCart(productId: number, productName: string) {
try {
await cartStore.addItem(productId)
notify(`${productName} added to cart`, 'success')
$logger.info('Item added to cart', { productId })
} catch (err) {
notify('Failed to add item', 'error')
$logger.error('Add to cart failed', { productId, err })
}
}
async function removeFromCart(productId: number) {
try {
await cartStore.removeItem(productId)
notify('Item removed from cart', 'info')
} catch (err) {
notify('Failed to remove item', 'error')
}
}
return {
items: computed(() => cartStore.items),
totalItems: computed(() => cartStore.totalItems),
totalPrice: computed(() => cartStore.totalPrice),
formattedTotal: computed(() => cartStore.formattedTotal),
isLoading: computed(() => cartStore.isLoading),
addToCart,
removeFromCart
}
}
Output:
// Execution Successful
6. Permissions Middleware Configuration
(1) ▶ Example: A Complete Middleware Architecture
// middleware/01-auth.global.ts
export default defineNuxtRouteMiddleware((to) => {
const publicPaths = ['/', '/products', '/about', '/login', '/register']
if (publicPaths.some(p => to.path === p || to.path.startsWith('/products'))) return
const token = useCookie('auth-token')
if (!token.value) return navigateTo(`/login?redirect=${encodeURIComponent(to.path)}`)
})
Output:
// Execution Successful
// middleware/admin.ts
export default defineNuxtRouteMiddleware((to) => {
const userCookie = useCookie('user-data')
if (!userCookie.value) return navigateTo('/login')
if ((userCookie.value as any).role !== 'admin') {
throw createError({ statusCode: 403, message: 'Admin access required' })
}
})
7. End-to-End Integration Testing of Pages
(1) ▶ Example: Product Detail Page (Complete Data Flow)
<!-- pages/products/[id].vue -->
<template>
<div v-if="product" class="product-detail">
<img :src="product.image" :alt="product.name" />
<div class="info">
<h1>{{ product.name }}</h1>
<p class="price">{{ formattedPrice }}</p>
<p>{{ product.reviewCount }} reviews</p>
<button @click="handleAddToCart" :disabled="adding">
{{ adding ? 'Adding...' : 'Add to Cart' }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const { data: product } = await useFetch(`/api/products/${route.params.id}`)
const { formatted } = usePriceFormat(computed(() => product.value?.price || 0))
const formattedPrice = formatted
const { addToCart } = useCart()
const adding = ref(false)
async function handleAddToCart() {
if (!product.value) return
adding.value = true
await addToCart(product.value.id, product.value.name)
adding.value = false
}
</script>
Output:
// Execution Successful
8. Integration Testing Checklist
| Test Item | Verification Method | Expected Result |
|---|---|---|
| API Available | curl /api/products | Returns product JSON |
| Store Status Sharing | Add item on the home page → View quantity on the details page | Quantities match |
| Composable Call | Click "Add to Cart" | Success: Added + Notification |
| Middleware Protection | Unauthenticated access to /admin | Redirect to /login |
| Plugin is running | View console | Logger output present |
| SSR Data Hydration | View Source Code | HTML Contains Product Data |
| Cookie Persistence | Refresh Page | Shopping Cart Data Retention |
❓ FAQ
addItem, use the return value to update the items array to ensure the Store and the backend remain in sync. Do not just modify the Store without calling the API.📖 Summary
- Complete shopping cart data flow: API → useCart → cartStore → page; data remains consistent at every stage
- Pinia Store updates its state using the return value after calling the API, ensuring synchronization between the client and the server.
- The
useCartComposable wraps the Store, API, and notifications; pages only need to calladdToCart - Global auth + "admin" middleware to implement comprehensive route authorization guards
- End-to-end integration testing: Verify each component—API, Store, Composable, Middleware, and Plugin—step by step
📝 Exercises
- Basic Exercise (Difficulty: ⭐): Complete the integration of the shopping cart API with cartStore so that the shopping cart count updates when the "Add to Cart" button is clicked on the product details page.
- Advanced Problem (Difficulty: ⭐⭐): Implement a complete middleware system: If an unauthenticated user accesses /checkout, redirect them to the login page; if a regular user accesses /admin, return a 403 error.
- Challenge (Difficulty: ⭐⭐⭐): Implement an end-to-end shopping process: Browse products → Add to cart → View cart → Checkout (simulate payment using the Stripe plugin), ensuring data consistency throughout the entire process.
---|



