Javascript is required
7 min read

A Comprehensive Guide to Data Fetching in Nuxt 3

A Comprehensive Guide to Data Fetching in Nuxt 3 Image

With Nuxt 3's rendering modes, you can execute API calls and render pages both on the client and server, which has some challenges. For example, we want to avoid duplicate network calls, efficient caching, and ensure that the calls work across environments. To address these challenges, Nuxt provides a built-in data fetching library ($fetch) and two composable (useFetch and useAsyncData).

In this article, I'll explain everything you need to know about the different data fetching methods available in Nuxt 3 and when to use them.

Data Fetching Library

Nuxt has a built-in library for data fetching: ofetch

ofetch is built on top of the fetch API and provides some handy features like:

  • Works on node, browser, and workers.
  • Smartly parses JSON and native values in the response.
  • Automatically throw errors when response.ok is false with a friendly error message and compact stack (hiding internals).
  • Automatically retries the request if an error happens.
  • You can provide async interceptors to hook into lifecycle events of ofetch calls.

You can use ofetch in your whole application with the $fetch alias:

const todos = await $fetch('/api/todos').catch((error) =>


useFetch is the most straightforward way to handle data fetching in a component setup function.

1<script setup>
2const { data, error, pending, refresh } = await useFetch('/api/todos')
6  <span v-if="pending">Loading...</span>
7  <span v-else-if="data">Todos: {{ data }}</span>
8  <span v-else-if="error">Error: {{ error }}</span>
9  <button @click="refresh">Refresh</button>

useFetch returns three reactive variables and a function:

  • data: a reactive variable that contains the result of the asynchronous function that is passed in.
  • error: a reactive error object containing information about the request error.
  • pending: a reactive boolean indicating whether the request is in progress.
  • refresh/execute: a function to refresh the data returned by the handler function. By default, Nuxt waits until a refresh is finished before it can be executed again.

The useFetch composable is responsible for forwarding the data to the client if the API call was executed on the server. This way, the client doesn't need to refetch the same data on the client side when the page hydrates. You can inspect this payload via useNuxtApp.payload(); the Nuxt DevTools visualize this data in the payload tab.

useFetch additionally reduces API calls by using a key to cache API responses. The key is automatically generated based on the URL and the fetch options. The useFetch composable is auto-imported and can be used in setup functions, lifecycle hooks, and plugin or route middleware.

You can use the value of a ref in the URL string to ensure your component updates when the reactive variable changes:

1const todoId = ref('uuid')
3const { data: tracks, pending, error } = useFetch(() => `/api/todos/${todoId.value}`)

If the todoId value changes, the URL will update accordingly and the data will be fetched again.

If you want, you can check out the official documentation for more information.


useFetch accepts a set of options as the last argument, which can be used to control the behavior of the composable.


Data-fetching composables will automatically wait for the asynchronous function to resolve before navigating to a new page when using Vue's Suspense. Nuxt uses Vue’s <Suspense> component under the hood to prevent navigation before every async data is available to the view.

However, if you want to bypass this behavior during client-side navigation, you can use the lazy option:

1<script setup>
2const { pending, data: todos } = useFetch('/api/todos', {
3  lazy: true,
8  <div v-if="pending">Loading ...</div>
9  <div v-else>
10    <div v-for="todo in todos">
11      {{ }}
12    </div>
13  </div>

In such cases, you'll need to handle the loading state manually by using the pending value.

Alternatively, you have the option of using useLazyFetch which is a convenient method to achieve the same result:

const { pending, data: todos } = useLazyFetch('/api/todos')


By default, data-fetching composables execute their asynchronous function in both client and server environments. To restrict the execution to the client side only, you can set the server option to false:

1const { pending, data: posts } = useFetch('/api/comments', {
2  lazy: true,
3  server: false,

This can be particularly useful when combined with the lazy option for data that is not required during the initial rendering, such as non-SEO sensitive data.


If you have not fetched data on the server, for instance using server: false, the data will not be fetched until the hydration process is complete.

This implies that even if you await useFetch on the client side, the data variable will continue to be null within <script setup>.

Minimize payload size

The pick option helps you minimize the payload size stored in your HTML document by selecting the fields you want to be returned from the composables:

1<script setup>
2const { data: todos } = await useFetch('/api/todos', {