Data Fetching
Daten vom Server laden — mit useFetch(), useAsyncData() und $fetch. Wie Controller-Actions in Rails, aber reaktiv und gecacht.
Daten laden in Nuxt
In Rails lädst du Daten im Controller und übergibst sie an die View. In Nuxt passiert das direkt in der Seite — mit speziellen Composables, die SSR, Caching und Reaktivität automatisch handhaben.
Rails-Vergleich: Controller → View
Rails: def index; @posts = Post.all; end → View rendert @posts
Nuxt: const { data: posts } = await useFetch('/api/posts') → Template rendert posts
Der Unterschied: In Nuxt ist alles in einer Datei, und die Daten sind reaktiv.
| Methode | Wann verwenden? |
|---|---|
useFetch() | Standard — für die meisten API-Calls |
useAsyncData() | Wenn du komplexe Logik brauchst |
$fetch() | Nur Client — z.B. Button-Clicks, Formulare |
useFetch() — Das Schweizer Taschenmesser
useFetch() ist dein Hauptwerkzeug. Es kombiniert Datenladen mit SSR-Support, Caching und Reaktivität — alles in einem Aufruf.
<template>
<div>
<!-- Ladezustand -->
<div v-if="status === 'pending'">Lade Posts...</div>
<!-- Fehlerfall -->
<div v-else-if="error">
Fehler: {{ error.message }}
</div>
<!-- Daten anzeigen -->
<ul v-else>
<li v-for="post in posts" :key="post.id">
<NuxtLink :to="'/posts/' + post.slug">
{{ post.title }}
</NuxtLink>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
// useFetch — wie @posts = Post.all im Rails-Controller
const {
data: posts,
error,
status
} = await useFetch('/api/posts')
</script>Was useFetch() zurückgibt
const {
data, // Ref<T | null> — die geladenen Daten
error, // Ref<Error | null> — Fehler, falls aufgetreten
status, // Ref<'idle' | 'pending' | 'success' | 'error'>
refresh, // () => Promise — Daten neu laden
execute, // () => Promise — manuell ausführen (bei immediate: false)
clear // () => void — Daten und Fehler zurücksetzen
} = await useFetch('/api/posts')SSR-Deduplizierung
useFetch() lädt die Daten auf dem Server, rendert HTML und schickt die Daten als Payload mit. Der Browser muss den API-Call nicht wiederholen. In Rails ist das automatisch (Server rendert immer), in SPAs ein häufiges Problem — Nuxt löst es elegant.
useFetch() mit Optionen
useFetch() akzeptiert viele Optionen für Query-Parameter, HTTP-Methoden, Header und Daten-Transformation.
<script setup lang="ts">
// GET mit Query-Parametern
const page = ref(1)
const { data: posts } = await useFetch('/api/posts', {
query: { page, limit: 10 } // → /api/posts?page=1&limit=10
// Reaktiv! Ändert sich page, werden Daten neu geladen
})
// POST mit Body
const { data: result } = await useFetch('/api/posts', {
method: 'POST',
body: { title: 'Neuer Post', content: '...' }
})
// Mit Headern und Transformation
const { data: userNames } = await useFetch('/api/users', {
headers: { 'Accept-Language': 'de' },
// Daten vor dem Cachen transformieren
transform: (users) => users.map(u => u.name)
})
// Nur bestimmte Felder aus der Antwort
const { data: titles } = await useFetch('/api/posts', {
pick: ['id', 'title'] // Reduziert den Payload
})
</script>useAsyncData() — Volle Kontrolle
Wenn du mehr Kontrolle brauchst oder mehrere API-Calls kombinieren willst, nutze useAsyncData(). Es ist die Basis unter useFetch().
<script setup lang="ts">
// Mehrere API-Calls kombinieren
const { data: dashboardData } = await useAsyncData(
'dashboard', // Eindeutiger Cache-Key
async () => {
// Parallele Requests — wie Promise.all
const [posts, users, stats] = await Promise.all([
$fetch('/api/posts'),
$fetch('/api/users'),
$fetch('/api/stats')
])
return { posts, users, stats }
}
)
// Zugriff: dashboardData.value.posts
// dashboardData.value.users
// dashboardData.value.stats
// In Rails wäre das:
// def dashboard
// @posts = Post.recent
// @users = User.active
// @stats = Stats.current
// end
</script>useFetch = useAsyncData + $fetch
useFetch(url) ist im Grunde nur eine Kurzform für useAsyncData(() => $fetch(url)). Wenn du nur einen API-Endpunkt aufrufst, nimm useFetch. Für alles Komplexere nimm useAsyncData.
$fetch — Für Client-Aktionen
Für Aktionen, die nicht beim Seitenaufbau passieren — Button-Clicks, Formular-Submissions, Löschvorgänge — nutzt du $fetch() direkt.
<template>
<div>
<h2>Neuen Post erstellen</h2>
<form @submit.prevent="createPost">
<input v-model="title" placeholder="Titel" />
<textarea v-model="content" placeholder="Inhalt" />
<button type="submit" :disabled="saving">
{{ saving ? 'Speichern...' : 'Erstellen' }}
</button>
</form>
</div>
</template>
<script setup lang="ts">
const title = ref('')
const content = ref('')
const saving = ref(false)
async function createPost() {
saving.value = true
try {
// $fetch für einmalige Aktionen
const post = await $fetch('/api/posts', {
method: 'POST',
body: {
title: title.value,
content: content.value
}
})
// Nach Erfolg: Weiterleiten
navigateTo('/posts/' + post.slug)
} catch (error) {
console.error('Erstellen fehlgeschlagen:', error)
} finally {
saving.value = false
}
}
</script>$fetch nicht im Setup verwenden!
Verwende $fetch() nicht im <script setup> Top-Level für initiale Daten. Das würde den Request sowohl auf dem Server als auch im Browser ausführen — doppelte Arbeit! Für initiale Daten immer useFetch() oder useAsyncData().
Lazy Fetching
Standardmäßig blockiert useFetch() die Navigation, bis die Daten geladen sind. Mit lazy: true wird die Seite sofort gerendert und die Daten nachgeladen.
<script setup lang="ts">
// Standard: Blockiert Navigation bis Daten da sind
const { data: posts } = await useFetch('/api/posts')
// Lazy: Seite wird sofort gerendert, Daten kommen nach
const { data: comments, status } = useFetch('/api/comments', {
lazy: true
})
// Hinweis: Kein 'await' bei lazy!
// Oder als Shortcut:
const { data: tags } = useLazyFetch('/api/tags')
</script>
<template>
<div>
<!-- Posts sind sofort da (await) -->
<PostList :posts="posts" />
<!-- Kommentare laden nach -->
<div v-if="status === 'pending'">
Lade Kommentare...
</div>
<CommentList v-else :comments="comments" />
</div>
</template>Rails-Vergleich: Turbo Frames
Lazy Fetching ist vergleichbar mit Turbo Frames in Rails, die Teile einer Seite nachladen: <turbo-frame src="/posts" loading="lazy">. In Nuxt ist es ein einfacher Parameter — kein HTML-Attribut, kein separater Request/Response-Cycle.
Refresh & Execute
Daten neu laden — nach einer Aktion, einem Timer oder einer Benutzerinteraktion:
<template>
<div>
<button @click="refresh()">
🔄 Aktualisieren
</button>
<ul>
<li v-for="post in posts" :key="post.id">
{{ post.title }}
<button @click="deleteAndRefresh(post.id)">
Löschen
</button>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
const { data: posts, refresh } = await useFetch('/api/posts')
async function deleteAndRefresh(id: number) {
// Post löschen
await $fetch(`/api/posts/${id}`, { method: 'DELETE' })
// Liste neu laden
await refresh()
}
// Auch nützlich: Daten beim Fokus-Wechsel neu laden
// const { data } = await useFetch('/api/notifications', {
// watch: [() => document.hasFocus()]
// })
</script>Error Handling
useFetch() gibt Fehler als reaktiven Wert zurück. Kein try/catch nötig — du kannst den Fehler direkt im Template anzeigen.
<template>
<div>
<!-- Globaler Fehler -->
<div v-if="error"
class="p-4 rounded-lg bg-red-100 border border-red-300">
<h3 class="font-bold text-red-800">Fehler beim Laden</h3>
<p class="text-red-700">{{ error.message }}</p>
<button @click="refresh()"
class="mt-2 px-4 py-2 bg-red-600 text-white rounded">
Erneut versuchen
</button>
</div>
<!-- Daten -->
<div v-else-if="post">
<h1>{{ post.title }}</h1>
<div v-html="post.content" />
</div>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const { data: post, error, refresh } = await useFetch(
`/api/posts/${route.params.id}`,
{
// Custom Error-Handling
onResponseError({ response }) {
if (response.status === 404) {
showError({ statusCode: 404, statusMessage: 'Post nicht gefunden' })
}
}
}
)
</script>Caching-Verhalten
Nuxt cached Daten automatisch mit einem eindeutigen Key. Wenn du dieselbe URL auf mehreren Seiten verwendest, wird nur einmal geladen.
// Automatischer Cache-Key (basiert auf URL + Params)
const { data } = await useFetch('/api/posts')
// Eigener Cache-Key
const { data } = await useFetch('/api/posts', {
key: 'all-posts'
})
// Cache umgehen
const { data } = await useFetch('/api/posts', {
key: 'posts-fresh',
getCachedData: () => undefined // Nie aus Cache lesen
})
// Reaktive Parameter → automatische neue Requests
const page = ref(1)
const { data: posts } = await useFetch('/api/posts', {
query: { page }
// page.value = 2 → neuer Request automatisch!
})Cache-Keys verstehen
Der Cache-Key wird automatisch aus der URL und den Parametern generiert. Wenn du reaktive Parameter verwendest (z.B. query: { page }), werden neue Daten bei Änderung automatisch geladen. Wie Rails.cache.fetch("posts-page-#{params[:page]}").
Praxis-Beispiel: Posts laden und anzeigen
Ein komplettes Beispiel, das Posts von einer API lädt, Fehler behandelt und eine Lade-Anzeige zeigt:
<template>
<div class="max-w-4xl mx-auto">
<h1 class="text-3xl font-bold mb-6">Blog</h1>
<!-- Suchfeld -->
<input
v-model="search"
type="search"
placeholder="Posts durchsuchen..."
class="w-full mb-6 px-4 py-2 rounded-lg border"
/>
<!-- Ladezustand -->
<div v-if="status === 'pending'"
class="text-center py-12 text-gray-500">
<p>Lade Posts...</p>
</div>
<!-- Fehlerzustand -->
<div v-else-if="error"
class="text-center py-12">
<p class="text-red-600 mb-4">{{ error.message }}</p>
<button @click="refresh()"
class="px-4 py-2 bg-blue-600 text-white rounded">
Erneut versuchen
</button>
</div>
<!-- Posts-Liste -->
<div v-else class="space-y-4">
<article v-for="post in posts" :key="post.id"
class="p-6 rounded-xl border hover:border-blue-300 transition">
<NuxtLink :to="'/posts/' + post.slug">
<h2 class="text-xl font-semibold mb-2">
{{ post.title }}
</h2>
<p class="text-gray-600">{{ post.excerpt }}</p>
<time class="text-sm text-gray-400 mt-2 block">
{{ new Date(post.createdAt).toLocaleDateString('de-DE') }}
</time>
</NuxtLink>
</article>
<!-- Pagination -->
<div class="flex justify-between mt-8">
<button @click="page--" :disabled="page <= 1">
← Zurück
</button>
<span>Seite {{ page }}</span>
<button @click="page++">
Weiter →
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const search = ref('')
const page = ref(1)
const { data: posts, error, status, refresh } = await useFetch(
'/api/posts',
{
query: { page, search }, // Reaktiv!
watch: [search] // Bei Suche neu laden
}
)
</script>// server/api/posts.get.ts
export default defineEventHandler(async (event) => {
const { page = '1', limit = '10', search = '' } = getQuery(event)
// In einer echten App: Datenbank-Abfrage
// Wie PostsController#index in Rails
let posts = [
{ id: 1, title: 'Einführung in Nuxt 3', slug: 'einfuehrung-nuxt-3',
excerpt: 'Alles über das Vue-Meta-Framework', createdAt: '2025-01-15' },
{ id: 2, title: 'Vue Composition API', slug: 'vue-composition-api',
excerpt: 'Reactive State Management leicht gemacht', createdAt: '2025-01-10' },
{ id: 3, title: 'Server Routes erklärt', slug: 'server-routes',
excerpt: 'API-Endpunkte direkt in Nuxt', createdAt: '2025-01-05' },
]
// Suche filtern
if (search) {
posts = posts.filter(p =>
p.title.toLowerCase().includes(String(search).toLowerCase())
)
}
// Pagination
const start = (Number(page) - 1) * Number(limit)
return posts.slice(start, start + Number(limit))
})Der komplette Vergleich
In Rails wäre das:PostsController#index → @posts = Post.page(params[:page])index.html.erb → HTML-Template mit @posts.each
In Nuxt passiert beides in einer Datei: Datenladen + Template. Die API-Route in server/api/ ersetzt den Rails-Controller.
Nächster Schritt
Du weißt jetzt, wie du Daten vom Server holst. Im nächsten Kapitel lernst du, wie du selbst Server-Routen (APIs) baust — das Backend in deiner Nuxt-App.