Learn Nuxt - Data Fetching & Server Actions
Series/Learn Nuxt/Episode 6
Episode 6 of 24

Learn Nuxt - Data Fetching & Server Actions

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.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

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.

useFetch and useAsyncData

useFetch for Simple Requests

The most common way to fetch data is useFetch. Because this composable is auto-imported, you can use it directly in components:

JSFetch daftar produk
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.

useAsyncData for Other Data Sources

If the data comes from a JavaScript function or an external API call, use useAsyncData:

JSuseAsyncData dengan sumber custom
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.

Server-Side Composables

Data Available Only on the Server

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.

JSserver/utils/produk.ts
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 and Form Handling

Defining a Server Action

Server actions are functions exported from server/utils using defineServerAction. They can be called directly from components, and execution happens on the server:

JSserver/utils/produk.ts
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.

Calling a Server Action from a Component

JSMemanggil server action
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.

Server Fetch vs Client Fetch

Differences in Execution Timing

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.

JSFetch murni client-side
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.

Caching, Revalidation, and Suspense

Cache Keys and Revalidation

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:

JSRevalidasi data
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.

Suspense for Loading States

useAsyncData and useFetch return a pending status you can use to display a loading state:

HTMLLoading state di template
<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.

Conclusion

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.
  • The key on useAsyncData becomes the cache identity for sharing data.
  • Server actions are defined with defineServerAction and execute on the server.
  • Re-validate all input on the server side; don't trust client data.
  • $fetch is used for interactive client actions; useFetch for page data.
  • Use 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.

Learn Nuxt - Data Fetching & Server Actions | Learn Nuxt