Étape 10 — Migrer fetch vers Axios + loading et erreurs
Objectifs
- Remplacer
fetch()par Axios dans le store Pinia - Afficher un squelette de chargement (
v-skeleton-loader) pendant les requêtes - Afficher une alerte (
v-alert) si l'API est indisponible - Créer un fichier mock JSON pour tester hors-ligne
- Comprendre les 3 états d'un appel API : loading, error, data
Résultat attendu

Contexte
Le store pokemonStore utilise encore fetch() pour charger les Pokémon. On va le migrer vers Axios pour profiter de la configuration centralisée (base URL, headers, gestion d'erreurs).
En parallèle, on va rendre l'interface robuste : au lieu d'un écran blanc quand l'API ne répond pas, l'utilisateur verra un message d'erreur clair.
Encart oral — Les 3 états d'un appel API
Tout appel réseau a exactement 3 états possibles :
- Loading — la requête est en cours (afficher un indicateur)
- Error — la requête a échoué (afficher un message d'erreur)
- Data — la requête a réussi (afficher les données)
Un bon développeur gère toujours ces 3 états. Ne jamais laisser l'utilisateur devant un écran vide sans explication.
Composants Vuetify utilisés
| Composant | Rôle | Documentation |
|---|---|---|
<v-skeleton-loader> | Affiche un placeholder animé pendant le chargement | Skeleton Loaders |
<v-alert> | Affiche un message d'information, d'erreur ou d'avertissement | Alerts |
Tâches
1. Migrer le store vers Axios
Dans src/stores/pokemonStore.js, remplacez fetch() par Axios.
Avant (fetch) :
async fetchPokemons ({ withLoader = true } = {}) {
if (withLoader) this.isLoading = true
try {
const response = await fetch('http://localhost:3535/pokemons')
this.pokemons = await response.json()
} catch (error) {
console.error('Erreur:', error.message)
this.pokemons = []
} finally {
if (withLoader) this.isLoading = false
}
}Après (Axios) :
import api from '@/plugins/axios'
// ... dans les actions :
async fetchPokemons ({ withLoader = true } = {}) {
if (withLoader) this.isLoading = true
try {
const response = await api.get('/pokemons')
this.pokemons = response.data
} catch (error) {
console.error('Erreur:', error.message)
this.pokemons = []
} finally {
if (withLoader) this.isLoading = false
}
}Faites la même migration pour fetchTypes :
async fetchTypes ({ withLoader = true } = {}) {
if (withLoader) this.isLoading = true
try {
const response = await api.get('/types')
this.types = response.data
} catch (error) {
console.error('Erreur:', error.message)
this.types = []
} finally {
if (withLoader) this.isLoading = false
}
}Notez les différences :
- Import :
import api from '@/plugins/axios'au lieu de rien (fetch est global) - Appel :
api.get('/pokemons')au lieu defetch('http://localhost:3535/pokemons') - Données :
response.dataau lieu deawait response.json()(Axios parse automatiquement le JSON) - URL :
/pokemonsau lieu de l'URL complète (grâce à labaseURL)
2. Ajouter le skeleton loader dans index.vue
Le state isLoading du store est déjà mis à true pendant les requêtes. On va l'utiliser pour afficher des squelettes de chargement.
Dans src/pages/index.vue, ajoutez avant la grille de cartes :
<!-- Squelettes de chargement pendant la requête API -->
<v-row v-if="pokemonStore.isLoading">
<v-col
v-for="n in 8"
:key="n"
cols="12"
sm="6"
md="4"
lg="3"
>
<v-skeleton-loader
type="image, article"
height="350"
/>
</v-col>
</v-row>Le squelette simule la forme des cartes Pokémon. Le type="image, article" dessine un rectangle (image) suivi de lignes de texte (titre + sous-titre).
3. Ajouter l'alerte d'erreur
Toujours dans index.vue, ajoutez une alerte qui s'affiche quand la liste est vide et que le chargement est terminé :
<!-- Message d'erreur si aucun Pokémon chargé -->
<v-alert
v-else-if="pokemonStore.pokemons.length === 0"
type="error"
variant="tonal"
class="mb-6"
>
Impossible de charger les Pokémon. Vérifiez que l'API tourne sur
{{ apiUrl }}.
</v-alert>
<!-- Grille de cartes (cas normal) -->
<v-row v-else>
<!-- ... vos v-col + PokemonCard ... -->
</v-row>Et dans le <script setup>, ajoutez la variable pour l'URL :
// URL de l'API pour le message d'erreur
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3535'Les trois blocs (v-if, v-else-if, v-else) couvrent les 3 états :
v-if="pokemonStore.isLoading" → Loading (skeleton)
v-else-if="pokemons.length === 0" → Error (alert)
v-else → Data (grille)4. Créer le fichier mock JSON
Créez public/mock.json avec quelques Pokémon de test :
{
"types": [
{ "id": 1, "name": "Normal", "color": "#A8A878" },
{ "id": 2, "name": "Feu", "color": "#F08030" },
{ "id": 3, "name": "Eau", "color": "#6890F0" },
{ "id": 4, "name": "Plante", "color": "#78C850" },
{ "id": 5, "name": "Électrique", "color": "#F8D030" }
],
"pokemons": [
{
"id": "1",
"name": "Pikachu",
"types": [5],
"level": 35,
"img": "pikachu.png",
"description": "Quand plusieurs de ces Pokémon se réunissent, leur énergie peut provoquer de violents orages.",
"stats": { "hp": 35, "attack": 55, "defense": 40, "speed": 90 }
},
{
"id": "2",
"name": "Bulbizarre",
"types": [4],
"level": 12,
"img": "bulbizarre.png",
"description": "Il a une étrange graine plantée sur son dos.",
"stats": { "hp": 45, "attack": 49, "defense": 49, "speed": 45 }
},
{
"id": "3",
"name": "Carapuce",
"types": [3],
"level": 8,
"img": "carapuce.png",
"description": "Son dos rond est recouvert d'une carapace qui lui sert de protection.",
"stats": { "hp": 44, "attack": 48, "defense": 65, "speed": 43 }
},
{
"id": "4",
"name": "Dracaufeu",
"types": [2],
"level": 60,
"img": "dracaufeu.png",
"description": "Il crache du feu si fort qu'il fait fondre les rochers.",
"stats": { "hp": 78, "attack": 84, "defense": 78, "speed": 100 }
},
{
"id": "5",
"name": "Rondoudou",
"types": [1],
"level": 20,
"img": "rondoudou.png",
"description": "Quand ses grands yeux s'illuminent, il chante une berceuse mystérieuse.",
"stats": { "hp": 115, "attack": 45, "defense": 20, "speed": 20 }
}
]
}Ce fichier est servi directement par Vite sur /mock.json. Il permet de tester l'application même si l'API est coupée, en changeant temporairement l'URL dans le store :
// Temporairement, pour tester sans API :
const response = await api.get('/mock.json')
this.pokemons = response.data.pokemons5. Tester les 3 états
- État normal : lancez l'API et l'app → les Pokémon s'affichent
- État loading : ouvrez DevTools → onglet Network → activez le throttle "Slow 3G" → rechargez la page → les squelettes s'affichent
- État error : arrêtez l'API (
Ctrl+Cdans le terminal de l'API) → rechargez la page → l'alerte d'erreur s'affiche
Tests
- Le store utilise
api.get()au lieu defetch() - Les squelettes s'affichent pendant le chargement
- L'alerte d'erreur s'affiche quand l'API est coupée
- L'application fonctionne normalement quand l'API tourne
- Le fichier
mock.jsonest accessible sur/mock.json - 0 erreurs dans la console (quand l'API tourne)
Solutions
src/stores/pokemonStore.js (actions modifiées)
import { defineStore } from 'pinia'
import api from '@/plugins/axios'
export const usePokemonStore = defineStore('pokemon', {
state: () => ({
isLoading: false,
types: [],
pokemons: [],
}),
getters: {
totalPokemons: state => state.pokemons.length,
getTypeById: state => typeId => {
return state.types.find(type => type.id === typeId)
},
getPokemonById: state => pokemonId => {
return state.pokemons.find(pokemon => pokemon.id === pokemonId)
},
},
actions: {
async init () {
console.log('Initialisation du store Pokémon...')
this.isLoading = true
try {
await Promise.all([
this.fetchTypes({ withLoader: false }),
this.fetchPokemons({ withLoader: false }),
])
console.log('Store Pokémon initialisé')
} catch (error) {
console.error('Erreur lors de l\'initialisation:', error)
} finally {
this.isLoading = false
}
},
async fetchTypes ({ withLoader = true } = {}) {
if (withLoader) this.isLoading = true
try {
const response = await api.get('/types')
if (response.data && response.data.data) {
this.types = response.data.data
} else if (response.data) {
this.types = response.data
} else {
this.types = []
}
} catch (error) {
console.error('Erreur lors du chargement des types:', error.message)
this.types = []
} finally {
if (withLoader) this.isLoading = false
}
},
async fetchPokemons ({ withLoader = true } = {}) {
if (withLoader) this.isLoading = true
try {
const response = await api.get('/pokemons')
if (response.data && response.data.data) {
this.pokemons = response.data.data
} else if (response.data) {
this.pokemons = response.data
} else {
this.pokemons = []
}
} catch (error) {
console.error('Erreur lors du chargement des Pokémon:', error.message)
this.pokemons = []
} finally {
if (withLoader) this.isLoading = false
}
},
},
})src/pages/index.vue (version avec les 3 états)
<template>
<v-container>
<h1 class="text-h3 text-center my-6">
Pokédex
<span class="text-subtitle-1">({{ pokemonStore.totalPokemons }})</span>
</h1>
<!-- État 1 : Loading — squelettes de chargement -->
<v-row v-if="pokemonStore.isLoading">
<v-col
v-for="n in 8"
:key="n"
cols="12"
sm="6"
md="4"
lg="3"
>
<v-skeleton-loader
type="image, article"
height="350"
/>
</v-col>
</v-row>
<!-- État 2 : Error — alerte si aucun Pokémon chargé -->
<v-alert
v-else-if="pokemonStore.pokemons.length === 0"
type="error"
variant="tonal"
class="mb-6"
>
Impossible de charger les Pokémon. Vérifiez que l'API tourne sur
{{ apiUrl }}.
</v-alert>
<!-- État 3 : Data — grille de cartes Pokémon -->
<v-row v-else>
<v-col
v-for="pokemon in pokemonStore.pokemons"
:key="pokemon.id"
cols="12"
sm="6"
md="4"
lg="3"
>
<pokemon-card :pokemon="pokemon" />
</v-col>
</v-row>
</v-container>
</template>
<script setup>
import { usePokemonStore } from '@/stores/pokemonStore'
import PokemonCard from '@/components/PokemonCard.vue'
const pokemonStore = usePokemonStore()
// URL de l'API pour le message d'erreur
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3535'
</script>Commit
git add -A
git commit -m "feat: migration Axios, skeleton loader et gestion erreurs API"