Vue.js

Appels API

Consommer une API REST depuis Vue 3 avec fetch natif

Dans ce chapitre, on connecte le Show Watchlist à une API REST publique : les films sont récupérés depuis TVMaze au lieu d'être déclarés en dur dans le composant.

Pourquoi appeler une API ?

Jusqu'ici, les données étaient déclarées directement dans le composant. En réalité, elles viennent d'un serveur distant via des appels HTTP.

Vue ne fournit pas de client HTTP intégré. Ce cours utilise fetch natif, disponible partout sans dépendance supplémentaire.

TVMaze API

TVMaze est une API publique de séries TV, gratuite et sans clé API. On l'utilise comme source de données pour notre watchlist.

EndpointDescription
GET /showsListe des séries (250 par page)
GET /shows/:idDétail d'une série
GET /search/shows?q=:queryRecherche de séries

Rappel : fetch natif

// GET
const response = await fetch('https://api.tvmaze.com/shows')
const shows = await response.json()

// Avec paramètre de recherche
const response = await fetch('https://api.tvmaze.com/search/shows?q=batman')
const results = await response.json() // [{ score, show }, ...]

fetch ne rejette pas la promesse en cas d'erreur HTTP (404, 500…). Il faut vérifier response.ok manuellement.

Type Show

On adapte le type à la structure retournée par TVMaze. Le champ seen est local (TVMaze ne le gère pas) :

// src/types/index.ts
export interface Show {
  id: number
  name: string
  genres: string[]
  summary: string | null
  image: { medium: string } | null
  rating: { average: number | null }
  seen: boolean
}

Appels API dans un composant

onMounted est le hook idéal pour déclencher un appel API au chargement d'une page. On gère localement les états isLoading et error.

<!-- src/pages/HomePage.vue -->
<template>
  <div>
    <p v-if="isLoading">Chargement...</p>
    <p v-else-if="error">{{ error }}</p>
    <ShowList v-else :shows="shows" @toggle-seen="onToggleSeen">
      <NSpace justify="space-between" align="center">
        <NInput v-model:value="search" placeholder="Rechercher une série..."/>
        <NTag type="success">{{ seenShows.length }} vu(s) sur {{ shows.length }}</NTag>
      </NSpace>
    </ShowList>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import type { Show } from '@/types'
import ShowList from '../components/ShowList.component.vue'

const BASE_URL = 'https://api.tvmaze.com'

const search = ref<string>('')
const shows = ref<Show[]>([])
const isLoading = ref<boolean>(false)
const error = ref<string | null>(null)

const seenShows = computed(() =>
  shows.value.filter((m) => m.seen)
)

const fetchShows = async () => {
  isLoading.value = true
  try {
    const res = await fetch(`${BASE_URL}/shows`)
    if (!res.ok) throw new Error(`Erreur ${res.status}`)
    const data: Omit<Show, 'seen'>[] = await res.json()
    shows.value = data.slice(0, 12).map((m) => ({ ...m, seen: false }))
  }
  catch (e) {
    error.value = e instanceof Error ? e.message : 'Erreur inconnue'
  }
  finally {
    isLoading.value = false
  }
}

watch(search, async (newVal) => {
  if (!newVal.trim()) {
    await fetchShows()
    return
  }
  isLoading.value = true
  try {
    const res = await fetch(`${BASE_URL}/search/shows?q=${encodeURIComponent(newVal)}`)
    if (!res.ok) throw new Error(`Erreur ${res.status}`)
    const results: { show: Omit<Show, 'seen'> }[] = await res.json()
    shows.value = results.map((r) => ({ ...r.show, seen: false }))
  }
  catch (e) {
    error.value = e instanceof Error ? e.message : 'Erreur inconnue'
  }
  finally {
    isLoading.value = false
  }
})

onMounted(fetchShows)

const onToggleSeen = (show: Show) => {
  show.seen = !show.seen
}
</script>

Quelques points importants :

  • data.slice(0, 12) limite l'affichage aux 12 premiers résultats
  • .map((m) => ({ ...m, seen: false })) ajoute le champ local seen
  • La recherche déclenche un nouvel appel à /search/shows?q=...
  • toggleSeen est purement local — TVMaze est une API en lecture seule