APIs
Una SPA Vue consume APIs HTTP para leer y mutar datos. El patron limpio: capa de cliente HTTP + composables/stores + estados loading/error/data en la UI.
fetch basico
typescript
export type Producto = { id: number; nombre: string; precio: number }
export async function getProductos(): Promise<Producto[]> {
const res = await fetch('/api/productos', {
headers: { Accept: 'application/json' },
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
return res.json()
}En desarrollo, configura proxy en Vite para evitar CORS:
typescript
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
},
})Composable useFetch
typescript
// composables/useFetch.ts
import { ref, watchEffect, type Ref } from 'vue'
export function useFetch<T>(url: Ref<string> | string) {
const data = ref<T | null>(null)
const error = ref<string | null>(null)
const loading = ref(false)
async function load(u: string) {
loading.value = true
error.value = null
try {
const res = await fetch(u)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = (await res.json()) as T
} catch (e) {
error.value = e instanceof Error ? e.message : 'Error de red'
data.value = null
} finally {
loading.value = false
}
}
watchEffect(() => {
const u = typeof url === 'string' ? url : url.value
if (u) void load(u)
})
return { data, error, loading, reload: () => load(typeof url === 'string' ? url : url.value) }
}vue
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useFetch } from '@/composables/useFetch'
import type { Producto } from '@/api/productos'
const route = useRoute()
const url = computed(() => `/api/productos/${route.params.id}`)
const { data, error, loading } = useFetch<Producto>(url)
</script>
<template>
<p v-if="loading">Cargando...</p>
<p v-else-if="error" role="alert">{{ error }}</p>
<article v-else-if="data">
<h1>{{ data.nombre }}</h1>
<p>{{ data.precio.toFixed(2) }} €</p>
</article>
</template>POST/PUT/DELETE con JSON
typescript
export async function crearProducto(input: Omit<Producto, 'id'>): Promise<Producto> {
const res = await fetch('/api/productos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify(input),
})
if (!res.ok) {
const body = await res.text()
throw new Error(body || `HTTP ${res.status}`)
}
return res.json()
}Headers de autenticacion
typescript
export function apiFetch(input: RequestInfo, init: RequestInit = {}) {
const token = localStorage.getItem('token')
const headers = new Headers(init.headers)
if (token) headers.set('Authorization', `Bearer ${token}`)
if (!headers.has('Accept')) headers.set('Accept', 'application/json')
return fetch(input, { ...init, headers })
}Si el backend usa cookies de sesion, configura credentials: 'include' y CORS acorde.
Abort y race conditions
typescript
import { onUnmounted, ref, watch } from 'vue'
const query = ref('')
const resultados = ref([])
let controller: AbortController | null = null
watch(query, async (q) => {
controller?.abort()
controller = new AbortController()
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, {
signal: controller.signal,
})
if (!res.ok) return
resultados.value = await res.json()
})
onUnmounted(() => controller?.abort())Cancela peticiones obsoletas al cambiar de ruta o de query.
Axios u ofetch
fetch nativo basta en la mayoria de casos. Axios aporta interceptores maduros; ofetch (Nuxt) tipa errores HTTP con comodidad. Elige una capa y no mezcles tres clientes.
Buenas practicas
- Tipa respuestas DTO; no uses
any. - Centraliza base URL y auth en un cliente.
- Distingue error de red, 4xx y 5xx en la UI.
- Usa proxy en dev; variables
import.meta.env.VITE_API_URLen prod. - Nunca expongas secretos de servidor en
VITE_*(van al bundle).
Errores habituales
- Ignorar
res.oky parsear JSON de un 500 como datos validos. - CORS mal entendido: el navegador bloquea; el proxy o el backend deben alinearse.
- Disparar fetch en el cuerpo de
setupsin cancelacion al desmontar. - Guardar el token solo en memoria y perder sesion al refrescar (o lo contrario: persistir sin plan de XSS).
- Hardcodear
http://localhost:3000en componentes.
Ejercicios
- Implementa
getProductosy una view con estados loading/error/lista. - Configura proxy
/apien Vite hacia tu backend local. - Anade
apiFetchcon Bearer token desde el auth store. - Cancela una busqueda anterior con
AbortControlleral escribir en el input.
Siguiente paso
En el capitulo 9 veras Vitest y Vue Test Utils para componentes, composables y stores.
