# Pokémon shopping cart with Vue 3, Axios and the PokéAPI

> How a Vue 3 shopping cart demo consumes the PokéAPI REST API: batched lazy loading with Promise.all, client-side pagination, a reactive cart with computed totals, loading and error states, and a simulated checkout.

## 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](https://pokeapi.co) 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.

<project-embed src="https://codepen.io/ps_cristopher/embed/NPKjGQX?default-tab=js%2Cresult&theme-id=dark" title="Pokémon shopping cart demo built with Vue 3">

The [live demo on CodePen](https://codepen.io/ps_cristopher/pen/NPKjGQX). Switch to the JS tab to read the full source.

</project-embed>

## 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:

```js
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.

```js
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:

```js
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 `ref` and `computed`.
- **Resilient UX** with loading and error states.
- **Responsive UI** with Tailwind CSS.

<style>

html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}

</style>

---

- [View on CodePen](https://codepen.io/ps_cristopher/pen/NPKjGQX)
- [PokéAPI](https://pokeapi.co)
