Load data with useFetch and useAsyncData, call your own APIs with $fetch, and control loading, refresh, and where it runs.
Why: useFetch is the simplest way to load data — it fetches once on the server (so the HTML arrives filled in), avoids re-fetching on the client, and gives you data plus loading/error state. Await it at the top of <script setup>.
<!-- app/pages/blog/index.vue -->
<script setup lang="ts">
const { data: posts, pending, error } = await useFetch('/api/posts')
</script>
<template>
<p v-if="pending">Loading…</p>
<p v-else-if="error">Something went wrong</p>
<ul v-else>
<li v-for="post in posts" :key="post.id">{{ post.title }}</li>
</ul>
</template>Why: when the data doesn’t come from a simple URL — a database call, several requests combined — wrap the work in useAsyncData. The first argument is a unique key Nuxt uses to cache and de-duplicate the result.
<script setup lang="ts">
const { data: user } = await useAsyncData('user', () => {
return $fetch('/api/me')
})
</script>
<template>
<p>Welcome, {{ user?.name }}</p>
</template>Why: useFetch is for loading data as a page renders. For a one-off call inside a handler (a button click, a form submit), use $fetch directly. Note: $fetch is auto-imported and works on both server and client.
<script setup lang="ts">
async function like(id: number) {
await $fetch('/api/like', { method: 'POST', body: { id } })
}
</script>
<template>
<button @click="like(1)">Like</button>
</template>Why: refresh() re-runs the fetch on demand (e.g. after a mutation). The lazy option lets the page render first and load the data after, instead of blocking navigation — good for non-critical data.
<script setup lang="ts">
const { data, refresh } = await useFetch('/api/posts', {
lazy: true, // don't block navigation; load after the page shows
})
// Call refresh() to fetch again, e.g. after creating a post
</script>
<template>
<button @click="() => refresh()">Reload</button>
</template>