Nuxt Data Fetching — useFetch, useAsyncData, and Server Data
In this tutorial, you will learn about Nuxt Data Fetching. We cover key concepts, practical examples, and best practices to help you master this topic.
Learn Nuxt 3 data fetching: useFetch for API calls, useAsyncData for custom fetching, and handling loading and error states with SSR support.
In this lesson, you'll understand how to fetch data in Nuxt 3, the difference between useFetch and useAsyncData, and how to handle Caching and deduplication.
What You'll Learn
How to use useFetch for simple API calls, useAsyncData for custom fetchers, handle loading/error states, and leverage Nuxt's built-in data caching.
Why It Matters
Nuxt's data fetching composables are optimized for SSR. They fetch data on the server, serialize it, and make it available client-side without double fetching.
flowchart LR
A[Page Request] --> B{SSR?}
B -->|Yes| C[Fetch on Server]
B -->|No| D[Fetch on Client]
C --> E[Serialize to JSON]
E --> F[Hydrate Client]
D --> G[Client Fetch]
style C fill:#00dc82,color:#fff
useFetch
The simplest way to fetch data:
<script setup>
const { data: posts, pending, error, refresh } = await useFetch('/api/posts', {
// Options
params: { page: 1 },
method: 'GET',
headers: { 'Authorization': `Bearer ${token}` }
});
// Auto-refresh every 30 seconds
const { data: liveData } = await useFetch('/api/live', {
refreshInterval: 30000
});
</script>
<template>
<div>
<div v-if="pending">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else>
<div v-for="post in posts" :key="post.id">{{ post.title }}</div>
</div>
</div>
</template>
Output: Posts are fetched during SSR (if page is server-rendered) or on the client. The component handles all states.
useAsyncData
For custom fetching logic:
<script setup>
const { data: transformedData, pending } = await useAsyncData('transformed', async () => {
const raw = await $fetch('/api/raw-data');
// Transform the data
return raw.map(item => ({
id: item.id,
label: item.name.toUpperCase(),
total: item.prices.reduce((a, b) => a + b, 0)
}));
});
</script>
<template>
<div>
<div v-for="item in transformedData" :key="item.id">
{{ item.label }}: {{ item.total }}
</div>
</div>
</template>
Output: Raw data is fetched and transformed server-side. The transformed result is available on the client without re-running the transformation.
useFetch vs useAsyncData
<script setup>
// useFetch — for direct API calls
const { data } = await useFetch('/api/posts');
// useAsyncData — for custom logic + $fetch
const { data } = await useAsyncData('posts', () => {
return $fetch('/api/posts');
});
// useAsyncData with transform
const { data } = await useAsyncData('posts', () => {
return $fetch('/api/posts');
}, {
transform: (posts) => posts.filter(p => p.published)
});
</script>
Use useFetch for simple GET requests. Use useAsyncData when you need custom fetching logic, data transformation, or non-GET requests.
Refetching and Cache
Control when data is refetched:
<script setup>
// Watch reactive sources and refetch on change
const page = ref(1);
const { data, refresh } = await useFetch('/api/posts', {
watch: [page] // Refetch when page changes
});
// Manual refresh
const handleRefresh = () => refresh();
// Disable initial fetch
const { execute } = await useFetch('/api/posts', {
immediate: false
});
</script>
<template>
<div>
<button @click="page++">Next Page</button>
<button @click="execute()">Load Data</button>
</div>
</template>
Error Handling
<script setup>
const { data, error } = await useFetch('/api/posts', {
onResponseError({ response }) {
console.error('API error:', response.status);
}
});
// Custom error handling
if (error.value) {
console.error('Failed to load:', error.value.message);
}
// Using $fetch for one-off calls with error handling
try {
const result = await $fetch('/api/submit', {
method: 'POST',
body: { title: 'New Post' }
});
} catch (err) {
console.error('Submission failed:', err);
}
</script>
Common Mistakes
- Not using
awaitwith data fetching composables: Withoutawait, the component renders before data is available, causing undefined errors. - Over-fetching on client navigation:
useFetchcaches by key. Use the same URL or custom key to reuse cached data. - Mutating
datadirectly: Usedata.value = newValueor callrefresh()to update. Direct mutation may not update the DOM. - Forgetting to handle
pendingstate: Without pending handling, users see blank content during loading. - Not using
$fetchfor API routes: For calling Nuxt server routes, use$fetchwith the route path. It's optimized for server-to-server calls.
Practice Questions
What is the difference between
useFetchanduseAsyncData? Answer:useFetchis a wrapper arounduseAsyncDatathat automatically infers key and type from the URL.useAsyncDataallows custom fetching logic.How does Nuxt prevent double data fetching? Answer: Data is fetched on the server during SSR, serialized into the HTML payload, and hydrated on the client. The client skips re-fetching.
What option controls automatic re-fetching? Answer: The
watchoption re-fetches when reactive sources change.refreshIntervalre-fetches on a timer.How do you manually trigger a data refresh? Answer: Call the
refresh()function returned byuseFetchoruseAsyncData.
Challenge
Create a search component that fetches results as the user types, debounced at 300ms. Use watch to refetch when the search query changes and show loading/empty/error states.
Mini Project
Build a paginated blog listing with: useFetch for fetching posts per page, refresh on page change, loading skeletons, error fallback with retry button, and pre-fetching the next page on hover.
FAQ
What's Next
Learn about NuxtLink and Navigation for client-side navigation with prefetching and active link styling.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro