This episode covers data fetching in Nuxt: useAsyncData and useFetch, server-side composables, server actions for form processing, the difference between fetching on the server and the client, and caching, revalidation, and suspense.

A store application is useless without data. Episode 6 covers the heart of Nuxt: how to fetch and send data. Nuxt provides built-in composables like useFetch and useAsyncData that work with SSR — data is fetched on the server, embedded into the page, then reused on the client without a second request.
Beyond reading data, this episode also introduces server actions: functions that run on the server but are called as if they were ordinary functions from the client. This changes how we process forms and mutate data — without explicitly writing an HTTP endpoint for every operation.
The most common way to fetch data is useFetch. Because this composable is auto-imported, you can use it directly in components:
const { data: produk, pending, error } = await useFetch("/api/produk")useFetch("/api/produk") works like a regular fetch, but its result is rendered on the server during SSR, embedded in the payload, then reused on the client. The pending and error objects help you show loading and failure states in the template.
If the data comes from a JavaScript function or an external API call, use useAsyncData:
const { data: total } = await useAsyncData("total-produk", () => {
return $fetch("/api/produk/total")
})useAsyncData accepts a unique key and a function that returns the data. That key becomes the cache identity — two uses with the same key share the same data. $fetch is Nuxt's wrapper around the standard fetch with parsing and error-handling features.
Nuxt has composables provided only for the server side, usually used in server routes. Examples: useSession to read a session, or database functions in server/utils. This kind of code must not leak to the client.
export async function getProdukPopuler() {
const items = await queryDatabase("SELECT * FROM produk LIMIT 5")
return items
}The queryDatabase function above is just an illustration — the important point is that anything only needed on the server should live in server/utils so it is never sent to the browser.
Server actions are functions exported from server/utils using defineServerAction. They can be called directly from components, and execution happens on the server:
export const tambahProduk = defineServerAction(
async (data: { nama: string; harga: number }) => {
const id = await simpanKeDatabase(data)
return { id }
}
)defineServerAction(async (data) => {...}) produces a function that is called from the client, runs the code on the server, then returns the result. Security matters here: re-validate input on the server side, never trust client data.
import { tambahProduk } from "~/server/utils/produk"
async function submit() {
const hasil = await tambahProduk({
nama: form.nama,
harga: form.harga,
})
}This pattern makes form processing very clean: no endpoint URL, no manual serialization. The server action becomes the single place where business logic runs.
useFetch runs on the server when the page is rendered, then on the client during hydration. This means the data is already present in the first HTML — great for SEO and time-to-content. However, for data that must always be fresh on the client, such as real-time notifications, prefer using $fetch directly in event handlers or in a composable that only runs on the client.
const daftar = ref([])
async function muatData() {
daftar.value = await $fetch("/api/notifikasi")
}$fetch inside a regular function runs only when called — in the browser. Make the distinction clear: use useFetch for page data that participates in SSR, $fetch for interactive actions.
Nuxt automatically stores useAsyncData and useFetch results in the SSR payload so the client doesn't repeat the request. To revalidate data after a mutation, use refresh or clearNuxtData:
const { data, refresh } = await useFetch("/api/produk")
async function hapusProduk(id) {
await $fetch(`/api/produk/${id}`, { method: "DELETE" })
await refresh()
}Call refresh() after data changes so the view follows the latest data without reloading the page.
useAsyncData and useFetch return a pending status you can use to display a loading state:
<template>
<div>
<p v-if="pending">Memuat data...</p>
<p v-else-if="error">Gagal memuat data</p>
<div v-else>
<KartuProduk v-for="item in produk" :key="item.id" v-bind="item" />
</div>
</div>
</template>The v-if pending, v-else-if error, and v-else pattern above is the standard way to handle the three data states in Nuxt.
Episode 6 opens the door to data: useFetch and useAsyncData for fetching data that gets rendered on the server, server actions for processing mutations safely, an understanding of the difference between server and client fetches, and the caching, revalidation, and suspense mechanisms for a good loading experience.
Key takeaways:
useFetch and useAsyncData fetch data on the server then reuse it on the client.useAsyncData becomes the cache identity for sharing data.defineServerAction and execute on the server.$fetch is used for interactive client actions; useFetch for page data.refresh and clearNuxtData for revalidation after mutations.In the next episode, episode 7, we will discuss state management — managing global state with Pinia, defining stores, handling hydration state in SSR, and Composition API patterns for shared state. Your store's shopping cart will have a proper home.