$fetch (ofetch)
$fetch (ofetch)
Level 5 — Data Fetching The globally available HTTP client in Nuxt 3. It is a highly optimized, isomorphic alternative to Axios or the native
fetchAPI, designed to parse JSON automatically and handle Server/Client execution flawlessly.
1. Prerequisites
- Universal Rendering (SSR) —
$fetchoperates seamlessly across both environments. - async / await — For asynchronous control flow.
2. Term Category
Data Fetching (Direct HTTP Fetch Utility): $fetch is Nuxt 3's built-in HTTP client (powered by ofetch) for imperative client-side or server-side API requests.
3. Explanation
Environment Context
- Server & Client
(1) Design Motivation — "Why did we design this?"
In modern web development, the native fetch() API has largely replaced older libraries like XMLHttpRequest or Axios. However, native fetch() has annoyances: you always have to manually call .json() on the response, and error handling requires manually checking res.ok.
Nuxt 3 solves this by replacing the native fetch with a library called ofetch (exposed globally as $fetch). It automatically parses JSON responses, automatically throws errors on 4xx/5xx status codes, and provides smart retries out of the box.
(2) Core Concept
You can use $fetch anywhere in your Nuxt application—inside components, composables, or Nitro server routes.
// No import needed, $fetch is globally available
try {
// $fetch automatically parses the JSON! No need for `await res.json()`
const users = await $fetch('/api/users', {
method: 'GET',
query: { limit: 10 } // automatically appends ?limit=10 to the URL
});
console.log(users);
} catch (error) {
// $fetch automatically throws if the server returns a 404 or 500
console.error("Failed to fetch users", error);
}
(3) The SSR Double-Fetch Problem
While $fetch is fantastic, you should almost never use it directly inside a Vue component's <script setup> during SSR.
Because the component runs twice (once on the Server, once on the Client), calling $fetch directly will cause the server to make the API request, and then the client to make the exact same API request a second later when it hydrates. This wastes bandwidth and causes UI flickering.
To fetch data inside a Vue component safely, you must wrap $fetch in useAsyncData, or use the shorthand useFetch.
4. Common Mistakes & Pitfalls
Mistake 1: Using $fetch for initial component data
The mistake: Calling $fetch directly in <script setup> to load the data required to render the page.
Why it's wrong: Nuxt does not magically serialize the result of $fetch to the client. The server fetches the data and renders the HTML. The client boots up, sees an empty variable, and fires a second network request to get the data again.
Golden Rule: Inside Vue components, always use useFetch. Reserve $fetch for user interactions (like a button click submitting a form) or inside Nitro server routes.
Incorrect:
<script setup>
// This runs on the server, AND AGAIN on the client!
const data = await $fetch('https://api.example.com/data');
</script>
Fix:
<script setup>
// This runs on the server, serializes the data, and skips the client fetch!
const { data } = await useFetch('https://api.example.com/data');
</script>
Mistake 2: Calling $fetch Direct Helper in Component Setup (Double Fetch Hydration Bug)
The mistake: Writing const data = await $fetch('/api/user') at top level of <script setup>.
Why it's wrong: $fetch executes on the server during SSR render, but does NOT serialize data into the Nuxt payload. When the client hydratest, it executes $fetch a second time (double fetch). Use useFetch or useAsyncData in component setup.
Incorrect:
<script setup>
const data = await $fetch('/api/users'); // ❌ Double fetch bug: executes on server AND client hydration!
</script>
Fix:
<script setup>
// Use useFetch in component setup to prevent double fetching:
const { data } = await useFetch('/api/users');
</script>
Mistake 3: Forgetting Content-Type: application/json Headers in $fetch POST Requests
The mistake: Passing raw JS object to $fetch without understanding of ofetch body auto-serialization.
Why it's wrong: $fetch (powered by ofetch) automatically sets Content-Type: application/json and serializes objects passed to body. Do NOT manually run JSON.stringify(body).
Incorrect:
await $fetch('/api/save', {
method: 'POST',
body: JSON.stringify({ name: 'Alice' }) // ❌ Double JSON serialization!
});
Fix:
await $fetch('/api/save', {
method: 'POST',
body: { name: 'Alice' } // Object auto-serialized by ofetch
});
5. Practice Exercises
Exercise 1: Executing Direct HTTP Requests with $fetch
Scenario:
Execute an imperative POST request using $fetch() inside an event handler method.
Requirements:
- Call
$fetch("/api/orders", { method: "POST", body: ... }).
Answer
Implementation
<script setup lang="ts">
const isSubmitting = ref(false);
async function submitOrder() {
isSubmitting.value = true;
try {
const response = await $fetch("/api/orders", {
method: "POST",
body: { productId: 42, quantity: 2 }
});
alert(`Order created! ID: ${response.id}`);
} catch (err: any) {
alert(`Order failed: ${err.message}`);
} finally {
isSubmitting.value = false;
}
}
</script>
<template>
<button @click="submitOrder" :disabled="isSubmitting">Submit Order</button>
</template>
Technical Explanation
$fetchis a low-level HTTP client designed for user-driven event handlers (button clicks, form submits).- Automatically parses JSON response bodies and handles HTTP status codes.
- Does NOT generate SSR payload cache entries like
useFetch().
Exercise 2: Understanding SSR Internal Direct Calls with $fetch
Scenario:
Explain why calling $fetch("/api/status") during SSR on the server executes a direct H3 event call without initiating a loopback TCP HTTP network request.
Requirements:
- Detail Nitro's direct event execution behavior.
Answer
Implementation
<script setup lang="ts">
// Executed on server: Nitro invokes /api/status handler directly in Node.js memory!
const status = await $fetch("/api/status");
</script>
<template>
<div>
<p>Internal Server Status: {{ status }}</p>
</div>
</template>
Technical Explanation
- On the server during SSR,
$fetchintercepts calls to local/api/endpoints and invokes Nitro H3 event handlers directly in RAM. - Eliminates loopback TCP network latency and socket overhead.
- Fast internal server execution architecture.
Exercise 3: Handling Custom HTTP Headers and Interceptors
Scenario:
Add a Bearer token Authorization header to a $fetch request using onRequest interceptor hooks.
Requirements:
- Pass
headersoronRequesthook to$fetch.
Answer
Implementation
const token = useCookie("auth_token");
const response = await $fetch("/api/user/profile", {
headers: {
Authorization: `Bearer ${token.value}`
},
onRequestError({ error }) {
console.error("Network request failed:", error);
}
});
Technical Explanation
$fetchaccepts standard Fetch API options (headers,query,method,body).onRequestErrorandonResponseErrorlifecycle interceptors handle custom network error processing.- Extensible HTTP request client.
6. Related Terms
useFetch— The SSR-safe wrapper around$fetch.useAsyncData— The underlying composable that handles SSR serialization.
7. Key Takeaways
$fetch(powered by ofetch) is Nuxt's global HTTP client.- It automatically parses JSON and throws on HTTP errors.
- It is perfect for form submissions and Nitro server routes.
- Never use
$fetchdirectly inside<script setup>for initial page data, as it will trigger a double-fetch on SSR.