Skip to content

Dominando $fetch en Nuxt 4 ​

La Herramienta de Bajo Nivel

Ejemplo básico ​

ts
const posts = await $fetch('https://api.example.com/posts');

¿Qué es $fetch? ​

$fetch es la función base de Nuxt para peticiones HTTP, basada en ofetch. A diferencia de useFetch, es una función pura de JavaScript sin reactividad integrada.

Características clave: ​

  • ✅ Funciona en cliente y servidor (isomórfica)
  • ✅ Sintaxis similar a fetch nativo pero con mejoras
  • ✅ Sin estados reactivos (pending, error automáticos)
  • ✅ Ideal para lógica de negocio, stores y acciones

TIP

Usar $fetch en componentes sin encapsularlo con useAsyncData provoca la doble obtención de datos: primero en el servidor y luego en el cliente durante la hidratación, ya que $fetch no transfiere el estado del servidor al cliente. Por lo tanto, la obtención se ejecutará en ambos lados, ya que el cliente debe obtener los datos de nuevo.

Sintaxis Básica ​

ts
const data = await $fetch(url, options);

Ejemplo práctico ​

ts
// In a component, composable or store
const posts = await $fetch('https://jsonplaceholder.typicode.com/posts');

// With error handling
try {
  const user = await $fetch('https://jsonplaceholder.typicode.com/users/1');
  console.log(user);
} catch (error) {
  console.error('Error fetching user:', error);
}

Diferencias Clave: $fetch vs useFetch ​

  • Nivel más bajo: No tiene estados reactivos (pending, error).
  • Flexible: Útil en stores, middleware, o cualquier lógica JS/TS.
  • Interceptores: Permite personalización global (como Axios).
Característica$fetchuseFetch
Reactividad❌ No✅ Sí (data, pending, error)
SSR Automático✅ Sí✅ Sí
Caché Automático❌ No✅ Sí
Uso RecomendadoLógica programáticaComponentes Vue
Interceptores✅ Sí (globales)❌ Limitado

Opciones de Configuración ​

Métodos HTTP y Body ​

ts
// POST request
const newPost = await $fetch('https://jsonplaceholder.typicode.com/posts', {
  method: 'POST',
  body: {
    title: 'My new post',
    body: 'Post content',
    userId: 1
  }
});

// PUT request
const updatedPost = await $fetch('https://jsonplaceholder.typicode.com/posts/1', {
  method: 'PUT',
  body: {
    id: 1,
    title: 'Updated title',
    body: 'Updated content',
    userId: 1
  }
});

// DELETE request
await $fetch('https://jsonplaceholder.typicode.com/posts/1', {
  method: 'DELETE'
});

Headers Personalizados ​

ts
const data = await $fetch('https://api.example.com/protected', {
  headers: {
    'Authorization': 'Bearer your-token-here',
    'Custom-Header': 'personalized value'
  }
});

Tiempo de espera y reintentos ​

ts
const data = await $fetch('https://api.example.com/data', {
  timeout: 5000, // 5 seconds
  retry: 3,      // 3 attempts in case of error
  retryDelay: 1000 // 1 second between attempts
});

Patrones Avanzados con $fetch ​

Crear Instancia Personalizada ​

ts
// plugins/api.ts
export default defineNuxtPlugin(() => {
  const api = $fetch.create({
    baseURL: 'https://jsonplaceholder.typicode.com',
    headers: {
      'Content-Type': 'application/json'
    },
    async onRequest({ request, options }) {
      // Add authentication token
      const token = localStorage.getItem('token');
      if (token) {
        options.headers = {
          ...options.headers,
          Authorization: `Bearer ${token}`
        };
      }
    },
    async onResponseError({ response }) {
      // Global error handling
      if (response.status === 401) {
        // Redirect to login
        navigateTo('/login');
      }
    }
  });

  return { provide: { api } };
});

Uso de la Instancia Personalizada: ​

ts
const { $api } = useNuxtApp();

// In any component or composable
const posts = await $api('/posts');
const user = await $api('/users/1', {
  method: 'POST',
  body: { name: 'New user' }
});

Combinar con useAsyncData para Reactividad ​

vue
<script setup>
const { data: posts, refresh } = await useAsyncData('posts', () => 
  $fetch('https://jsonplaceholder.typicode.com/posts')
);

const deletePost = async (id) => {
  await $fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
    method: 'DELETE'
  });
  refresh(); // Refresh data after deletion
};
</script>

Uso en Stores (Pinia) ​

ts
// stores/posts.ts
export const usePostsStore = defineStore('posts', () => {
  const posts = ref([]);
  
  const fetchPosts = async () => {
    posts.value = await $fetch('https://jsonplaceholder.typicode.com/posts');
  };
  
  const createPost = async (postData) => {
    const newPost = await $fetch('https://jsonplaceholder.typicode.com/posts', {
      method: 'POST',
      body: postData
    });
    posts.value.push(newPost);
  };
  
  return { posts, fetchPosts, createPost };
});

Manejo de Errores Detallado ​

ts
try {
  const data = await $fetch('https://jsonplaceholder.typicode.com/invalid-endpoint', {
    retry: 2,
    onRequestError({ error }) {
      console.error('Error in request:', error);
    },
    onResponseError({ response }) {
      console.error('Error in response:', response.status, response.statusText);
    }
  });
} catch (error) {
  if (error.statusCode === 404) {
    console.error('Resource not found');
  } else if (error.statusCode === 500) {
    console.error('Server error');
  } else {
    console.error('Unexpected error:', error);
  }
}

Migración desde Axios ​

Si vienes de Axios, aquí las equivalencias:

Interceptores Axios → $fetch ​

ts
// Axios
axios.interceptors.request.use((config) => {
  config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// $fetch equivalent
const api = $fetch.create({
  async onRequest({ options }) {
    options.headers = {
      ...options.headers,
      Authorization: `Bearer ${localStorage.getItem('token')}`
    };
  }
});

Métodos Comunes ​

ts
// Axios
await axios.get('/posts');
await axios.post('/posts', data);
await axios.put('/posts/1', data);
await axios.delete('/posts/1');

// $fetch
await $fetch('/posts');
await $fetch('/posts', { method: 'POST', body: data });
await $fetch('/posts/1', { method: 'PUT', body: data });
await $fetch('/posts/1', { method: 'DELETE' });

Mejores Prácticas con $fetch ​

  • ✅ Centraliza la configuración con $fetch.create()
  • ✅ Usa try/catch para manejo de errores explícito
  • ✅ Combina con useAsyncData cuando necesites reactividad
  • ✅ Usa en stores/composables para lógica reutilizable
  • ❌ Evita usar directamente en templates (mejor usar useFetch)

Ejemplo Completo: CRUD con $fetch ​

ts
// composables/usePosts.ts
export const usePosts = () => {
  const config = useRuntimeConfig();
  const baseURL = config.public.apiBase;

  const fetchAll = () => $fetch(`${baseURL}/posts`);
  
  const fetchOne = (id: number) => $fetch(`${baseURL}/posts/${id}`);
  
  const create = (postData: any) => $fetch(`${baseURL}/posts`, {
    method: 'POST',
    body: postData
  });
  
  const update = (id: number, postData: any) => $fetch(`${baseURL}/posts/${id}`, {
    method: 'PUT',
    body: postData
  });
  
  const remove = (id: number) => $fetch(`${baseURL}/posts/${id}`, {
    method: 'DELETE'
  });

  return { fetchAll, fetchOne, create, update, remove };
};

Conclusión ​

$fetch es tu herramienta de bajo nivel para:

  • ✅ Lógica programática compleja
  • ✅ Stores y composables
  • ✅ Personalización avanzada (interceptores)
  • ✅ Migración desde Axios

¿Siguiente paso? useAsyncData para combinar la potencia de $fetch con la reactividad de Nuxt.