Skip to content

17. Manejo de APIs y Fetch

En Vue 3 con Composition API, tenemos dos opciones principales para realizar peticiones HTTP: la API nativa fetch y la librería axios. Ambas pueden utilizarse dentro de la función setup() para obtener datos de APIs externas.

<script setup>
import { ref, onMounted } from 'vue'
// Estados para manejar la petición
const data = ref(null)
const loading = ref(false)
const error = ref(null)
// Función para obtener datos
const fetchData = async () => {
loading.value = true
error.value = null
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1')
if (!response.ok) {
throw new Error('Error en la petición')
}
data.value = await response.json()
} catch (err) {
error.value = err.message || 'Error desconocido'
} finally {
loading.value = false
}
}
// Ejecutar la petición al montar el componente
onMounted(() => {
fetchData()
})
</script>
<template>
<div>
<div v-if="loading">Cargando...</div>
<div v-else-if="error">Error: {{ error }}</div>
<div v-else-if="data">
<h2>{{ data.title }}</h2>
<p>{{ data.body }}</p>
</div>
</div>
</template>

Para usar Axios, primero necesitas instalarlo:

Terminal window
npm install axios
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
// Estados para manejar la petición
const data = ref(null)
const loading = ref(false)
const error = ref(null)
// Función para obtener datos
const fetchData = async () => {
loading.value = true
error.value = null
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/posts/1')
data.value = response.data
} catch (err) {
error.value = err.response?.data?.message || err.message || 'Error desconocido'
} finally {
loading.value = false
}
}
onMounted(() => {
fetchData()
})
</script>
<template>
<div>
<div v-if="loading">Cargando...</div>
<div v-else-if="error">Error: {{ error }}</div>
<div v-else-if="data">
<h2>{{ data.title }}</h2>
<p>{{ data.body }}</p>
</div>
</div>
</template>

Manejar correctamente los estados de una petición API es fundamental para una buena experiencia de usuario. En Composition API, usamos refs para controlar estos estados:

<script setup>
import { ref } from 'vue'
// Estados para manejar el ciclo de vida de la petición
const data = ref(null) // Almacena los datos recibidos
const loading = ref(false) // Indica si la petición está en curso
const error = ref(null) // Almacena errores si ocurren
// Función genérica para peticiones
const useApi = async (url, options = {}) => {
loading.value = true
error.value = null
try {
const response = await fetch(url, options)
if (!response.ok) {
throw new Error('Error HTTP: ' + response.status)
}
data.value = await response.json()
return data.value
} catch (err) {
error.value = err.message
console.error('Error en la petición:', err)
return null
} finally {
loading.value = false
}
}
</script>

En el template, puedes usar estos estados para mostrar diferentes contenidos según el estado de la petición:

<template>
<div class="api-container">
<!-- Estado de carga -->
<div v-if="loading" class="loading-state">
<div class="spinner"></div>
<p>Cargando datos...</p>
</div>
<!-- Estado de error -->
<div v-else-if="error" class="error-state">
<p>❌ Error: {{ error }}</p>
<button @click="fetchData">Reintentar</button>
</div>
<!-- Estado con datos -->
<div v-else-if="data" class="data-state">
<h2>Datos recibidos:</h2>
<pre>{{ JSON.stringify(data, null, 2) }}</pre>
</div>
<!-- Estado inicial -->
<div v-else class="empty-state">
<p>No hay datos disponibles</p>
<button @click="fetchData">Cargar datos</button>
</div>
</div>
</template>

Una de las ventajas de Composition API es la capacidad de crear peticiones reactivas que respondan a cambios en el estado. Podemos usar watch o watchEffect para esto:

<script setup>
import { ref, watch } from 'vue'
import axios from 'axios'
// Estados
const userId = ref(1)
const userData = ref(null)
const loading = ref(false)
const error = ref(null)
// Función para obtener datos del usuario
const fetchUserData = async (id) => {
loading.value = true
error.value = null
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/users/' + id)
userData.value = response.data
} catch (err) {
error.value = err.message
userData.value = null
} finally {
loading.value = false
}
}
// Observar cambios en userId y ejecutar la petición
watch(userId, (newId) => {
fetchUserData(newId)
}, { immediate: true }) // immediate:true ejecuta la función inmediatamente al montar
</script>
<template>
<div>
<div class="controls">
<label>Seleccionar usuario ID:</label>
<input type="number" v-model.number="userId" min="1" max="10" />
</div>
<div v-if="loading">Cargando información del usuario...</div>
<div v-else-if="error">Error: {{ error }}</div>
<div v-else-if="userData" class="user-card">
<h2>{{ userData.name }}</h2>
<p>Email: {{ userData.email }}</p>
<p>Teléfono: {{ userData.phone }}</p>
</div>
</div>
</template>

watchEffect es una alternativa más concisa cuando queremos ejecutar código reactivo que depende de múltiples valores:

<script setup>
import { ref, watchEffect } from 'vue'
import axios from 'axios'
// Estados
const searchQuery = ref('')
const category = ref('all')
const results = ref([])
const loading = ref(false)
const error = ref(null)
// Función para buscar productos
const searchProducts = async () => {
// No buscar si el query está vacío
if (!searchQuery.value.trim()) {
results.value = []
return
}
loading.value = true
error.value = null
try {
// Construir URL con parámetros de búsqueda
const url = new URL('https://api.example.com/products')
url.searchParams.append('q', searchQuery.value)
if (category.value !== 'all') {
url.searchParams.append('category', category.value)
}
const response = await axios.get(url.toString())
results.value = response.data
} catch (err) {
error.value = err.message
results.value = []
} finally {
loading.value = false
}
}
// Debounce para evitar demasiadas peticiones
let timeout = null
const debouncedSearch = () => {
clearTimeout(timeout)
timeout = setTimeout(() => {
searchProducts()
}, 300)
}
// Ejecutar búsqueda cuando cambie el query o la categoría
watchEffect(() => {
// Acceder a las refs dentro de watchEffect las hace dependencias
const query = searchQuery.value
const cat = category.value
// Solo ejecutar si hay un query
if (query) {
debouncedSearch()
}
})
</script>
<template>
<div>
<div class="search-controls">
<input
type="text"
v-model="searchQuery"
placeholder="Buscar productos..."
/>
<select v-model="category">
<option value="all">Todas las categorías</option>
<option value="electronics">Electrónica</option>
<option value="clothing">Ropa</option>
<option value="books">Libros</option>
</select>
</div>
<div v-if="loading">Buscando...</div>
<div v-else-if="error">Error: {{ error }}</div>
<div v-else-if="results.length > 0" class="results-grid">
<div v-for="item in results" :key="item.id" class="product-card">
<h3>{{ item.name }}</h3>
<p>{{ item.price }} €</p>
</div>
</div>
<div v-else-if="searchQuery.trim()" class="no-results">
No se encontraron resultados
</div>
</div>
</template>

1. Centralizar la lógica de API en composables

Section titled “1. Centralizar la lógica de API en composables”

Crea composables reutilizables para tus llamadas API:

useApi.js
import { ref } from 'vue'
import axios from 'axios'
export function useApi() {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const execute = async (url, options = {}) => {
loading.value = true
error.value = null
try {
const response = await axios(url, options)
data.value = response.data
return response.data
} catch (err) {
error.value = err.response?.data?.message || err.message
return null
} finally {
loading.value = false
}
}
return {
data,
loading,
error,
execute
}
}

Uso en componentes:

<script setup>
import { useApi } from '@/composables/useApi'
const { data, loading, error, execute } = useApi()
// Usar el composable
const fetchUsers = () => {
execute('https://api.example.com/users')
}
</script>
useCachedApi.js
import { ref } from 'vue'
import axios from 'axios'
export function useCachedApi(cacheTime = 5 * 60 * 1000) { // 5 minutos por defecto
const cache = new Map()
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const execute = async (url, options = {}) => {
const cacheKey = url + '-' + JSON.stringify(options)
const cachedData = cache.get(cacheKey)
// Usar datos en caché si existen y no han expirado
if (cachedData && Date.now() - cachedData.timestamp < cacheTime) {
data.value = cachedData.data
return cachedData.data
}
loading.value = true
error.value = null
try {
const response = await axios(url, options)
data.value = response.data
// Guardar en caché
cache.set(cacheKey, {
data: response.data,
timestamp: Date.now()
})
return response.data
} catch (err) {
error.value = err.response?.data?.message || err.message
return null
} finally {
loading.value = false
}
}
return {
data,
loading,
error,
execute,
clearCache: () => cache.clear()
}
}
useAuthApi.js
import { ref } from 'vue'
import axios from 'axios'
export function useAuthApi() {
const instance = axios.create()
const data = ref(null)
const loading = ref(false)
const error = ref(null)
// Interceptor para añadir token
instance.interceptors.request.use(config => {
const token = localStorage.getItem('auth_token')
if (token) {
config.headers.Authorization = 'Bearer ' + token
}
return config
})
// Interceptor para manejar errores 401 (token expirado)
instance.interceptors.response.use(
response => response,
async error => {
if (error.response?.status === 401) {
// Intentar refrescar token o redirigir a login
localStorage.removeItem('auth_token')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
const execute = async (url, options = {}) => {
loading.value = true
error.value = null
try {
const response = await instance(url, options)
data.value = response.data
return response.data
} catch (err) {
error.value = err.response?.data?.message || err.message
return null
} finally {
loading.value = false
}
}
return {
data,
loading,
error,
execute
}
}
<script setup>
import { ref, onUnmounted } from 'vue'
import axios from 'axios'
const data = ref(null)
const loading = ref(false)
const error = ref(null)
// Crear token de cancelación
const controller = new AbortController()
const fetchData = async () => {
loading.value = true
error.value = null
try {
const response = await axios.get('https://api.example.com/data', {
signal: controller.signal // Pasar la señal para posible cancelación
})
data.value = response.data
} catch (err) {
if (axios.isCancel(err)) {
console.log('Petición cancelada:', err.message)
} else {
error.value = err.message
}
} finally {
loading.value = false
}
}
// Cancelar peticiones pendientes al desmontar el componente
onUnmounted(() => {
controller.abort('Componente desmontado')
})
</script>
useApiError.js
import { ref, computed } from 'vue'
export function useApiError() {
const apiError = ref(null)
const setError = (error) => {
if (!error) {
apiError.value = null
return
}
// Normalizar diferentes tipos de errores
if (error.response) {
// Error de respuesta del servidor
const { status, data } = error.response
apiError.value = {
code: status,
message: data.message || 'Error ' + status,
details: data.errors || null
}
} else if (error.request) {
// Error de red (sin respuesta)
apiError.value = {
code: 'NETWORK_ERROR',
message: 'Error de conexión. Comprueba tu red.',
details: null
}
} else {
// Error de configuración
apiError.value = {
code: 'REQUEST_ERROR',
message: error.message || 'Error desconocido',
details: null
}
}
}
const clearError = () => {
apiError.value = null
}
const errorMessage = computed(() => apiError.value?.message || null)
const hasError = computed(() => apiError.value !== null)
return {
apiError,
setError,
clearError,
errorMessage,
hasError
}
}
🐝