Data Fetching
Nuxt comes with two composables and a built-in library to perform data-fetching in browser or server environments:
useFetch
,
useAsyncData
and
$fetch
.
In a nutshell:
-
$fetchis the simplest way to make a network request. -
useFetchis a wrapper around$fetchthat fetches data only once in universal rendering . -
useAsyncDatais similar touseFetchbut offers more fine-grained control.
Both
useFetch
and
useAsyncData
share a common set of options and patterns that we will detail in the last sections.
Nuxt is a framework which can run isomorphic (or universal) code in both server and client environments. If the
$fetch
function
is used to perform data fetching in the setup function of a Vue component, this may cause data to be fetched twice, once on the server (to render the HTML) and once again on the client (when the HTML is hydrated). This can cause hydration issues, increase the time to interactivity and cause unpredictable behavior.
The
useFetch
and
useAsyncData
composables solve this problem by ensuring that if an API call is made on the server, the data is forwarded to the client in the payload.
The payload is a JavaScript object accessible through
useNuxtApp().payload
. It is used on the client to avoid refetching the same data when the code is executed in the browser
during hydration
.
<script setup lang="ts">
const { data } = await useFetch('/api/data')
async function handleFormSubmit () {
const res = await $fetch('/api/submit', {
method: 'POST',
body: {
// My form data
</script>
<template>
<div v-if="data == undefined">
No data
</div>
<div v-else>
<form @submit="handleFormSubmit">
<!-- form input tags -->
</form>
</div>
</template>
In the example above,
useFetch
would make sure that the request would occur in the server and is properly forwarded to the browser.
$fetch
has no such mechanism and is a better option to use when the request is solely made from the browser.
Nuxt uses Vue's
<Suspense>
component under the hood to prevent navigation before every async data is available to the view. The data fetching composables can help you leverage this feature and use what suits best on a per-call basis.
Nuxt includes the
ofetch
library, and is auto-imported as the
$fetch
alias globally across your application.
<script setup lang="ts"> async functionaddTodo
When calling
useFetch
on the server, Nuxt will use
useRequestFetch
to proxy client headers and cookies (with the exception of headers not meant to be forwarded, like
host
).
<script setup lang="ts">
const { data } = await useFetch('/api/echo')
</script>
// /api/echo.ts
export default defineEventHandler(event => parseCookies(event))
Alternatively, the example below shows how to use
useRequestHeaders
to access and send cookies to the API from a server-side request (originating on the client). Using an isomorphic
$fetch
call, we ensure that the API endpoint has access to the same
cookie
header originally sent by the user's browser. This is only necessary if you aren't using
useFetch
.
<script setup lang="ts">
const headers = useRequestHeaders(['cookie'])
async function getCurrentUser () {
return await $fetch('/api/me', { headers })
</script>
The
useFetch
composable uses
$fetch
under-the-hood to make SSR-safe network calls in the setup function.
<script setup lang="ts">
const { data
This composable is a wrapper around the
useAsyncData
composable and
$fetch
utility.
The
useAsyncData
composable is responsible for wrapping async logic and returning the result once it is resolved.
There are some cases when using the
useFetch
composable is not appropriate, for example when a CMS or a third-party provide their own query layer. In this case, you can use
useAsyncData
to wrap your calls and still keep the benefits provided by the composable.
<script setup lang="ts">
const { data, error } = await useAsyncData('users', () => myGetFunction('users'))
// This is also possible:
const { data, error } = await useAsyncData(() => myGetFunction('users'))
</script>
<script setup lang="ts">
const { id } = useRoute().params
const { data, error } = await useAsyncData(`user:${id}`, () => {
return myGetFunction('users', { id })
</script>
The
useAsyncData
composable is a great way to wrap and wait for multiple
$fetch
requests to be completed, and then process the results.
<script setup lang="ts">
const { data: discounts, status } = await useAsyncData('cart-discount', async (_nuxtApp, { signal }) => {
const [coupons, offers] = await Promise.all([
$fetch('/cart/coupons', { signal }),
$fetch('/cart/offers', { signal }),
return { coupons, offers }
// discounts.value.coupons
// discounts.value.offers
</script>
useFetch
and
useAsyncData
have the same return values listed below.
-
data: the result of the asynchronous function that is passed in. -
refresh/execute: a function that can be used to refresh the data returned by thehandlerfunction. -
clear: a function that can be used to setdatatoundefined(or the value ofoptions.default()if provided), seterrortoundefined, setstatustoidle, and mark any currently pending requests as cancelled. -
error: an error object if the data fetching failed. -
status: a string indicating the status of the data request ("idle","pending","success","error").
By default, Nuxt waits until a
refresh
is finished before it can be executed again.
useAsyncData
and
useFetch
return the same object type and accept a common set of options as their last argument. They can help you control the composables behavior, such as navigation blocking, caching or execution.
By default, data fetching composables will wait for the resolution of their asynchronous function before navigating to a new page by using Vue's Suspense. This feature can be ignored on client-side navigation with the
lazy
option. In that case, you will have to manually handle loading state using the
status
value.
<script setup lang="ts">
const { status
You can alternatively use
useLazyFetch
and
useLazyAsyncData
as convenient methods to perform the same.
<script setup lang="ts">
const { status
By default, data fetching composables will perform their asynchronous function on both client and server environments. Set the
server
option to
false
to only perform the call on the client-side. On initial load, the data will not be fetched before hydration is complete so you have to handle a pending state, though on subsequent client-side navigation the data will be awaited before loading the page.
Combined with the
lazy
option, this can be useful for data that is not needed on the first render (for example, non-SEO sensitive data).
/* This call is performed before hydration */
const articles
The
useFetch
composable is meant to be invoked in setup method or called directly at the top level of a function in lifecycle hooks, otherwise you should use
$fetch
method
.
The
pick
option helps you to minimize the payload size stored in your HTML document by only selecting the fields that you want returned from the composables.
<script setup lang="ts">
/* only pick the fields used in your template */
const { data: mountain } = await useFetch('/api/mountains/everest', {
pick: ['title', 'description'],
</script>
<template>
<h1>{{ mountain.title }}</h1>
<p>{{ mountain.description }}</p>
</template>
If you need more control or map over several objects, you can use the
transform
function to alter the result of the query.
const { data: mountains } = await useFetch('/api/mountains', {
transform: (mountains) => {
return mountains.map(mountain => ({ title: mountain.title, description: mountain.description }))
Keys
Keys
useFetch
and
useAsyncData
use keys to prevent refetching the same data.
-
useFetchuses the provided URL as a key. Alternatively, akeyvalue can be provided in theoptionsobject passed as a last argument. -
useAsyncDatauses its first argument as a key if it is a string. If the first argument is the handler function that performs the query, then a key that is unique to the file name and line number of the instance ofuseAsyncDatawill be generated for you.
Shared State and Option Consistency
Shared State and Option Consistency
When multiple components use the same key with
useAsyncData
or
useFetch
, they will share the same
data
,
error
and
status
refs. This ensures consistency across components but requires some options to be consistent.
The following options
must be consistent
across all calls with the same key:
-
handlerfunction -
deepoption -
transformfunction -
pickarray -
getCachedDatafunction -
defaultvalue
// ❌ This will trigger a development warning
const { data: users1 } = useAsyncData('users', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }), { deep: false })
const { data: users2 } = useAsyncData('users', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }), { deep: true })
The following options
can safely differ
without triggering warnings:
-
server -
lazy -
immediate -
dedupe -
watch
// ✅ This is allowed
const { data: users1 } = useAsyncData('users', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }), { immediate: true })
const { data: users2 } = useAsyncData('users', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }), { immediate: false })
If you need independent instances, use different keys:
// These are completely independent instances
const { data: users1 } = useAsyncData('users-1', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }))
const { data: users2 } = useAsyncData('users-2', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }))
Reactive Keys
Reactive Keys
You can use computed refs, plain refs or getter functions as keys, allowing for dynamic data fetching that automatically updates when dependencies change:
// Using a computed property as a key
const userId = ref('123')
const { data: user } = useAsyncData(
computed(() => `user-${userId.value}`),
() => fetchUser(userId.value),
// When userId changes, the data will be automatically refetched
// and the old data will be cleaned up if no other components use it
userId.value = '456'
Refresh and execute
Refresh and execute
If you want to fetch or refresh data manually, use the
execute
or
refresh
function provided by the composables.
<script setup lang="ts">
const { data
The
execute
function is an alias for
refresh
that works in exactly the same way but is more semantic for cases when the fetch is
not immediate
.
Clear
Clear
If you want to clear the data provided, for whatever reason, without needing to know the specific key to pass to
clearNuxtData
, you can use the
clear
function provided by the composables.
<script setup lang="ts">
const { data
Watch
Watch
To re-run your fetching function each time other reactive values in your application change, use the
watch
option. You can use it for one or multiple
watchable
elements.
<script setup lang="ts">
const id
Note that
watching a reactive value won't change the URL fetched
. For example, this will keep fetching the same initial ID of the user because the URL is constructed at the moment the function is invoked.
<script setup lang="ts">
const id = ref(1)
const { data, error, refresh } = await useFetch(`/api/users/${id.value}`, {
watch: [id],
</script>
If you need to change the URL based on a reactive value, you may want to use a
computed URL
instead.
When reactive fetch options are provided, they'll be automatically watched and trigger refetches. In some cases, it can be useful to opt-out of this behavior by specifying
watch: false
.
const id = ref(1)
// Won't automatically refetch when id changes
const { data, execute } = await useFetch('/api/users', {
query: { id }, // id is watched by default
watch: false, // disables automatic watching of id
// doesn't trigger refetch
id.value = 2
Computed URL
Computed URL
Sometimes you may need to compute a URL from reactive values, and refresh the data each time these change. Instead of juggling your way around, you can attach each param as a reactive value. Nuxt will automatically use the reactive value and re-fetch each time it changes.
<script setup lang="ts">
const id = ref(null)
const { data, status } = useLazyFetch('/api/user', {
query: {
user_id: id,
</script>
In the case of more complex URL construction, you may use a callback as a
computed getter
that returns the URL string.
Every time a dependency changes, the data will be fetched using the newly constructed URL. Combine this with
not-immediate
, and you can wait until the reactive element changes before fetching.
<script setup lang="ts">
const id = ref(null)
const { data, status } = useLazyFetch(() => `/api/users/${id.value}`, {
immediate: false,
const pending = computed(() => status.value === 'pending')
</script>
<template>
<!-- disable the input while fetching -->
<input
v-model="id"
type="number"
:disabled="pending"
<div v-if="status === 'idle'">
Type a user ID
</div>
<div v-else-if="pending">
Loading ...
</div>
<div v-else>
{{ data }}
</div>
</div>
</template>
If you need to force a refresh when other reactive values change, you can also
watch other values
.
The
useFetch
composable will start fetching data the moment is invoked. You may prevent this by setting
immediate: false
, for example, to wait for user interaction.
With that, you will need both the
status
to handle the fetch lifecycle, and
execute
to start the data fetch.
<script setup lang="ts">
const { data, error, execute, status } = await useLazyFetch('/api/comments', {
immediate: false,
</script>
<template>
<div v-if="status === 'idle'">
<button @click="execute">
Get data
</button>
</div
>
<div v-else-if="status === 'pending'">
Loading comments...
</div>
<div v-else>
{{ data }}
</div>
</template>
For finer control, the
status
variable can be:
-
idlewhen the fetch hasn't started -
pendingwhen a fetch has started but not yet completed -
errorwhen the fetch fails -
successwhen the fetch is completed successfully
When we call
$fetch
in the browser, user headers like
cookie
will be directly sent to the API.
Normally, during server-side-rendering, due to security considerations, the
$fetch
wouldn't include the user's browser cookies, nor pass on cookies from the fetch response.
However, when calling
useFetch
with a relative URL on the server, Nuxt will use
useRequestFetch
to proxy headers and cookies (with the exception of headers not meant to be forwarded, like
host
).
If you want to pass on/proxy cookies in the other direction, from an internal request back to the client, you will need to handle this yourself.
import { appendResponseHeader } from 'h3'
import type { H3Event } from 'h3'
export const fetchWithCookie = async (event: H3Event, url: string) => {
/* Get the response from the server endpoint */
const res = await $fetch.raw(url)
/* Get the cookies from the response */
const cookies = res.headers.getSetCookie()
/* Attach each cookie to our incoming Request */
for (const cookie of cookies) {
appendResponseHeader(event, 'set-cookie', cookie)
/* Return the data of the response */
return res._data
<script setup lang="ts">
// This composable will automatically pass cookies to the client
const event = useRequestEvent()
const { data: result } = await useAsyncData(() => fetchWithCookie(event!, '/api/with-cookie'))
onMounted(() => console.log(document.cookie))
</
script>
Nuxt provides a way to perform
asyncData
fetching within the Options API. You must wrap your component definition within
defineNuxtComponent
for this to work.
<script>
export default defineNuxtComponent({
/* Use the fetchKey option to provide a unique key */
fetchKey: 'hello',
async asyncData () {
return {
hello: await $fetch('/api/hello'),
</script>
When using
useAsyncData
and
useLazyAsyncData
to transfer data fetched on server to the client (as well as anything else that utilizes
the Nuxt payload
), the payload is serialized with
devalue
. This allows us to transfer not just basic JSON but also to serialize and revive/deserialize more advanced kinds of data, such as regular expressions, Dates, Map and Set,
ref
,
reactive
,
shallowRef
,
shallowReactive
and
NuxtError
- and more.
It is also possible to define your own serializer/deserializer for types that are not supported by Nuxt. You can read more in the
useNuxtApp
docs.
When fetching data from the
server
directory, the response is serialized using
JSON.stringify
. However, since serialization is limited to only JavaScript primitive types, Nuxt does its best to convert the return type of
$fetch
and
useFetch
to match the actual value.
export default defineEventHandler(() => {
return new Date()
<script setup lang="ts">
// Type of `data` is inferred as string even though we returned a Date object
const { data } = await useFetch('/api/foo')
</script>
To customize the serialization behavior, you can define a
toJSON
function on your returned object. If you define a
toJSON
method, Nuxt will respect the return type of the function and will not try to convert the types.
export default defineEventHandler(() => {
const data = {
createdAt: new Date(),
toJSON () {
return {
createdAt: {
year: this.createdAt.getFullYear(),
month: this.createdAt.getMonth(),
day: this.createdAt.getDate(),
return data
<script setup lang="ts">
// Type of `data` is inferred as
// createdAt: {
// year: number
// month: number
// day: number
// }
const { data } = await useFetch('/api/bar')
</script>
Nuxt does not currently support an alternative serializer to
JSON.stringify
. However, you can return your payload as a normal string and utilize the
toJSON
method to maintain type safety.
In the example below, we use
superjson
as our serializer.
import superjson from 'superjson'
export default defineEventHandler(() => {
const data = {
createdAt: new Date(),
// Workaround the type conversion
toJSON () {
return this
// Serialize the output to string, using superjson
return superjson.stringify(data) as unknown as typeof data
<script setup lang="ts">
import superjson from 'superjson'
// `date` is inferred as { createdAt: Date } and you can safely use the Date object methods
const { data } = await useFetch('/api/superjson', {
transform: (value) => {
return superjson.parse(value as unknown as string)
</script>
When consuming SSE via POST request, you need to handle the connection manually. Here's how you can do it:
// Make a POST request to the SSE endpoint
const response = await $fetch<ReadableStream>('/chats/ask-ai', {
method: 'POST',
body: {
query: 'Hello AI, how are you?',
responseType: 'stream',
// Create a new ReadableStream from the response with TextDecoderStream to get the data as text
const reader = response.pipeThrough(new TextDecoderStream()).getReader()
// Read the chunk of data as we get it
while (true) {
const { value, done } = await reader.read()
if (done) { break }
console.log('Received:', value)
When requests don't rely on each other, you can make them in parallel with
Promise.all()
to boost performance.
const { data } = await useAsyncData((_nuxtApp, { signal }) => {
return Promise.all([
$fetch('/api/comments/', { signal }),
$fetch('/api/author/12', { signal }),