Het patroon

Data ophalen van een API doe je bijna altijd op één van deze momenten:

  • Bij het laden van een component → in onMounted
  • Na een gebruikersactie (klik, submit) → in een event handler
  • Als een URL-parameter verandert → in een watch

Je gebruikt de browser's ingebouwde fetch() met async/await — geen extra library nodig.

Volgorde: zorg dat je de basis van refs, onMounted en async/await begrijpt voor je hier instapt.

GET — data ophalen bij laden

Het patroon in drie stappen

<script setup>
import { ref, onMounted } from 'vue'

// 1. Ref voor de data
const users = ref([])

// 2. Async functie die fetcht
const loadUsers = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/users')
  users.value = await response.json()
}

// 3. Roep aan op het juiste moment
onMounted(() => {
  loadUsers()
})
</script>

<template>
  <ul>
    <li v-for="user in users" :key="user.id">
      {{ user.name }}
    </li>
  </ul>
</template>

Korter: async direct in onMounted

<script setup>
import { ref, onMounted } from 'vue'

const users = ref([])

onMounted(async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/users')
  users.value = await response.json()
})
</script>

Wat gebeurt er?

  1. Component verschijnt op het scherm
  2. onMounted start de fetch
  3. Server stuurt JSON terug
  4. users.value wordt geüpdatet
  5. Vue's reactivity zorgt dat de v-for meteen rendert

POST — data versturen

Bij een POST stuur je extra opties mee aan fetch(): de method, headers en de body.

<script setup>
import { ref } from 'vue'

const title = ref('')
const result = ref(null)

const createPost = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      title: title.value,
      userId: 1
    })
  })

  result.value = await response.json()
  title.value = ''
}
</script>

<template>
  <form @submit.prevent="createPost">
    <input v-model="title" placeholder="Titel" />
    <button type="submit">Versturen</button>
  </form>

  <pre v-if="result">{{ result }}</pre>
</template>

Belangrijk:

  • method: 'POST' — vertel de server wat je wilt
  • Content-Type header — vertel dat je JSON stuurt
  • JSON.stringify() — zet je object om naar JSON-tekst

Andere methodes

Voor PUT (updaten) en DELETE werkt het hetzelfde — alleen method verandert:

// Updaten
fetch('/api/posts/5', {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ title: 'Nieuwe titel' })
})

// Verwijderen
fetch('/api/posts/5', {
  method: 'DELETE'
})

Errors afhandelen

Een fetch kan fout gaan: server is down, geen internet, of de server geeft een 404/500. Vang dat af met try / catch.

<script setup>
import { ref, onMounted } from 'vue'

const users = ref([])
const error = ref(null)

onMounted(async () => {
  try {
    const response = await fetch('https://api.example.com/users')

    if (!response.ok) {
      throw new Error(`Server gaf ${response.status}`)
    }

    users.value = await response.json()
  } catch (e) {
    error.value = e.message
  }
})
</script>

<template>
  <p v-if="error" style="color: red">{{ error }}</p>
  <ul v-else>
    <li v-for="user in users" :key="user.id">{{ user.name }}</li>
  </ul>
</template>

Let op die response.ok check: fetch werpt geen error bij een 404 of 500. Je moet zelf checken of de response succesvol was. Alleen netwerkfouten (geen internet, server onbereikbaar) gooien automatisch een error.

Voor het complete loading + error + data patroon → zie Loading & Error States.

Fetch als URL-parameter verandert

Op een detailpagina /user/:id wil je een nieuwe fetch doen elke keer dat de id verandert (bijv. gebruiker klikt op een andere link in een lijst).

Combineer useRoute met een watch die immediate: true heeft:

<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()
const user = ref(null)

const loadUser = async (id) => {
  const res = await fetch(`/api/users/${id}`)
  user.value = await res.json()
}

watch(() => route.params.id, (newId) => {
  loadUser(newId)
}, { immediate: true })   // ← ook bij eerste render
</script>

Waarom immediate: true? Standaard wacht watch tot de waarde verandert. Met immediate draait hij ook meteen bij setup — dan heb je geen aparte onMounted meer nodig.

Belangrijke Regels

  • GET bij laden → in onMounted
  • POST/PUT/DELETE na actie → in een event handler (@submit, @click)
  • Altijd response.json() om data te krijgen (response zelf is een Response-object, niet je data)
  • Check response.ok voor 4xx/5xx errors
  • Wrap in try/catch voor netwerkfouten
  • POST met bodyJSON.stringify() + Content-Type header

Veelgemaakte Fouten

Fout — response.json() vergeten:

const res = await fetch('/api/users')
users.value = res                    // ❌ res is een Response-object
console.log(users.value)             // → [object Response]

Goed:

const res = await fetch('/api/users')
users.value = await res.json()       // ✅

Fout — body zonder stringify:

fetch('/api/posts', {
  method: 'POST',
  body: { title: 'Test' }    // ❌ object — server begrijpt het niet
})

Goed:

fetch('/api/posts', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ title: 'Test' })   // ✅
})

Fout — vergeten await:

onMounted(() => {
  const res = fetch('/api/users')   // ❌ res is een Promise
  users.value = res
})

Goed — async + await:

onMounted(async () => {
  const res = await fetch('/api/users')   // ✅
  users.value = await res.json()
})