Skip to content

Nuxt Data Fetching — useFetch, useAsyncData, and Server Data

DodaTech Updated 2026-06-28 4 min read

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

  1. Not using await with data fetching composables: Without await, the component renders before data is available, causing undefined errors.
  2. Over-fetching on client navigation: useFetch caches by key. Use the same URL or custom key to reuse cached data.
  3. Mutating data directly: Use data.value = newValue or call refresh() to update. Direct mutation may not update the DOM.
  4. Forgetting to handle pending state: Without pending handling, users see blank content during loading.
  5. Not using $fetch for API routes: For calling Nuxt server routes, use $fetch with the route path. It's optimized for server-to-server calls.

Practice Questions

  1. What is the difference between useFetch and useAsyncData? Answer: useFetch is a wrapper around useAsyncData that automatically infers key and type from the URL. useAsyncData allows custom fetching logic.

  2. 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.

  3. What option controls automatic re-fetching? Answer: The watch option re-fetches when reactive sources change. refreshInterval re-fetches on a timer.

  4. How do you manually trigger a data refresh? Answer: Call the refresh() function returned by useFetch or useAsyncData.

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

Can I use `useFetch` with POST requests?

: Yes. Use the method option: useFetch('/api/submit', { method: 'POST', body: data }).

Does `useFetch` support interceptors?

: No built-in interceptors. Use $fetch with ofetch interceptors or create a custom composable wrapper.

How do I type `useFetch` responses?

: Use generics: useFetch<Post[]>('/api/posts'). The data ref will be typed as Post[] | null.

Can I cancel in-flight requests?

: Yes. useFetch returns an abortController that can abort the request on component unmount or manual cancellation.

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