What does the demo do?
This is a shopping cart demo that uses Pokémon as products. It's built with Vue 3, the Composition API, Axios and Tailwind CSS, and it gets its catalog from the public PokéAPI REST API.
Features:
- A product catalog fetched dynamically from a REST API.
- Lazy loading and client-side pagination: products load in batches of 10, up to 100.
- A reactive cart that stays in sync with the product list.
- Quantity management: add, increase, decrease and remove items.
- Computed totals for item count and price.
- Loading and error states for every request.
- A responsive layout and a simulated checkout flow.
How does it fetch products from the PokéAPI?
The PokéAPI list endpoint (/api/v2/pokemon?limit=10&offset=0) only returns each Pokémon's name and URL. The image, types and stats need a separate request per Pokémon.
Making those requests one after another would be slow, so the app runs them in parallel with Promise.all. A whole batch then resolves in about the time of its slowest request:
import axios from 'axios'
const BATCH_SIZE = 10
const MAX_PRODUCTS = 100
const fetchBatch = async (offset) => {
const { data } = await axios.get('https://pokeapi.co/api/v2/pokemon', {
params: { limit: BATCH_SIZE, offset },
})
return Promise.all(
data.results.map(({ url }) => axios.get(url).then(res => res.data))
)
}
Simplified for this article. See the CodePen for the exact implementation.
How do lazy loading and pagination work?
The app doesn't request all 100 products up front. It loads 10 at a time and fetches the next batch as the user pages through the catalog. This keeps the first load fast and avoids sending traffic to a free public API for data nobody looks at.
import { ref, computed } from 'vue'
const products = ref([])
const loading = ref(false)
const error = ref(null)
const canLoadMore = computed(() => products.value.length < MAX_PRODUCTS)
const loadMore = async () => {
if (loading.value || !canLoadMore.value) return
loading.value = true
error.value = null
try {
const batch = await fetchBatch(products.value.length)
products.value.push(...batch.map(toProduct))
} catch (e) {
error.value = 'Could not load products. Please try again.'
} finally {
loading.value = false
}
}
The loading guard stops a second request for the same batch from starting while one is still in flight. Using finally means the loading state is always cleared, even when a request fails.
How is the cart state managed?
The raw API responses are transformed into a small product shape that the UI needs (id, name, image, price). The cart is a reactive list of products with quantities. Totals are computed, never stored, so they can't fall out of sync:
const cart = ref([])
const addToCart = (product) => {
const item = cart.value.find(item => item.id === product.id)
if (item) item.quantity++
else cart.value.push({ ...product, quantity: 1 })
}
const totalItems = computed(() =>
cart.value.reduce((sum, item) => sum + item.quantity, 0)
)
const totalPrice = computed(() =>
cart.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
Because the catalog and the cart share the same product id, the product list and the cart stay in sync.
What happens at checkout?
The checkout is simulated. It completes the shopping flow from browsing to ordering without a real payment provider, and no payment data is collected.
What frontend concepts does it demonstrate?
- REST API integration with Axios and async/await.
- Performance-conscious API use: batching, parallel requests with
Promise.all, lazy loading and a hard limit. - Data transformation from API responses into a UI-friendly model.
- Reactive state management with
refandcomputed. - Resilient UX with loading and error states.
- Responsive UI with Tailwind CSS.

