Vue.js Nuxt.js API Architecture

Patterns para consumir APIs en Vue/Nuxt

Pablo Alcalde García

Patterns para consumir APIs en Vue/Nuxt

Así gestiono las llamadas a APIs en mis proyectos de producción.

Pattern 1: Composable useApi

// composables/useApi.js
export function useApi(url, options = {}) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)
  
  async function execute() {
    loading.value = true
    error.value = null
    
    try {
      const response = await $fetch(url, options)
      data.value = response
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  }
  
  return { data, error, loading, execute }
}

Uso

<script setup>
const { data: users, loading, execute } = useApi('/api/users')

onMounted(() => execute())
</script>

<template>
  <div v-if="loading">Cargando...</div>
  <div v-else-if="users">
    <div v-for="user in users" :key="user.id">
      {{ user.name }}
    </div>
  </div>
</template>

Pattern 2: Service Layer

// services/api/users.js
class UserService {
  constructor(httpClient) {
    this.http = httpClient
  }
  
  async getAll(params = {}) {
    return this.http.get('/users', { params })
  }
  
  async getById(id) {
    return this.http.get(`/users/${id}`)
  }
  
  async create(data) {
    return this.http.post('/users', data)
  }
  
  async update(id, data) {
    return this.http.put(`/users/${id}`, data)
  }
  
  async delete(id) {
    return this.http.delete(`/users/${id}`)
  }
}

export const userService = new UserService($fetch)

Uso

// En componente
const users = await userService.getAll({ limit: 10 })

Pattern 3: Nuxt useFetch

<script setup>
const { data: users, pending, error, refresh } = await useFetch('/api/users', {
  key: 'users',
  transform: (data) => data.users,
  pick: ['id', 'name', 'email'],
  watch: [searchQuery],
  onResponse({ response }) {
    console.log('Response:', response)
  }
})
</script>

Pattern 4: Interceptors

// plugins/api.js
export default defineNuxtPlugin(() => {
  const api = $fetch.create({
    baseURL: '/api',
    
    // Request interceptor
    onRequest({ options }) {
      const token = useCookie('auth-token')
      
      if (token.value) {
        options.headers = {
          ...options.headers,
          Authorization: `Bearer ${token.value}`
        }
      }
    },
    
    // Response interceptor
    onResponse({ response }) {
      // Log todas las respuestas
      console.log('API Response:', response.status)
    },
    
    // Error interceptor
    onResponseError({ response }) {
      if (response.status === 401) {
        navigateTo('/login')
      }
    }
  })
  
  return {
    provide: {
      api
    }
  }
})

Pattern 5: Reactive Queries

// composables/useUsers.js
export function useUsers(filters = {}) {
  const filtersRef = ref(filters)
  const enabled = ref(true)
  
  const { data, pending, error, refresh } = useFetch('/api/users', {
    query: filtersRef,
    immediate: enabled,
    watch: [filtersRef]
  })
  
  const users = computed(() => data.value?.users || [])
  const total = computed(() => data.value?.total || 0)
  
  return {
    users,
    total,
    loading: pending,
    error,
    refresh,
    filters: filtersRef
  }
}

Uso

<script setup>
const filters = ref({ role: 'admin' })
const { users, total, loading } = useUsers(filters)

// Cambiar filtros reactivamente
function filterByRole(role) {
  filters.value.role = role
}
</script>

Pattern 6: Optimistic Updates

export function useUpdateUser() {
  const users = useState('users')
  
  async function update(id, data) {
    // Guardar estado original
    const original = users.value.find(u => u.id === id)
    
    // Update optimista
    const index = users.value.findIndex(u => u.id === id)
    users.value[index] = { ...original, ...data }
    
    try {
      await $fetch(`/api/users/${id}`, {
        method: 'PUT',
        body: data
      })
    } catch (error) {
      // Revertir en caso de error
      users.value[index] = original
      throw error
    }
  }
  
  return { update }
}

Pattern 7: Polling

export function usePolling(url, interval = 5000) {
  const data = ref(null)
  const error = ref(null)
  let timer = null
  
  async function fetch() {
    try {
      data.value = await $fetch(url)
      error.value = null
    } catch (e) {
      error.value = e
    }
  }
  
  function start() {
    fetch()
    timer = setInterval(fetch, interval)
  }
  
  function stop() {
    clearInterval(timer)
  }
  
  onMounted(start)
  onUnmounted(stop)
  
  return { data, error, start, stop }
}

Pattern 8: Infinite Scroll

export function useInfiniteScroll(endpoint) {
  const items = ref([])
  const page = ref(1)
  const hasMore = ref(true)
  const loading = ref(false)
  
  async function loadMore() {
    if (loading.value || !hasMore.value) return
    
    loading.value = true
    
    try {
      const data = await $fetch(endpoint, {
        params: { page: page.value }
      })
      
      items.value.push(...data.items)
      hasMore.value = data.hasMore
      page.value++
    } finally {
      loading.value = false
    }
  }
  
  return { items, loadMore, hasMore, loading }
}

Pattern 9: Cache con useState

export async function getCachedUsers() {
  const users = useState('users', () => null)
  
  if (!users.value) {
    users.value = await $fetch('/api/users')
  }
  
  return users
}

Pattern 10: Error Handling

export function useApiError() {
  const toast = useToast()
  
  function handleError(error) {
    const message = {
      400: 'Solicitud inválida',
      401: 'No autorizado',
      403: 'Sin permisos',
      404: 'No encontrado',
      500: 'Error del servidor'
    }[error.status] || 'Error desconocido'
    
    toast.error(message)
    
    // Log para debugging
    if (process.dev) {
      console.error('API Error:', error)
    }
  }
  
  return { handleError }
}

Mi stack en producción

// Combinación de patterns
export function useUserCRUD() {
  const { data: users, refresh } = useFetch('/api/users', {
    key: 'users',
    transform: (data) => data.users
  })
  
  const { handleError } = useApiError()
  
  async function create(userData) {
    try {
      await $fetch('/api/users', {
        method: 'POST',
        body: userData
      })
      await refresh()
    } catch (error) {
      handleError(error)
    }
  }
  
  async function update(id, userData) {
    try {
      await $fetch(`/api/users/${id}`, {
        method: 'PUT',
        body: userData
      })
      await refresh()
    } catch (error) {
      handleError(error)
    }
  }
  
  async function remove(id) {
    try {
      await $fetch(`/api/users/${id}`, {
        method: 'DELETE'
      })
      await refresh()
    } catch (error) {
      handleError(error)
    }
  }
  
  return { users, create, update, remove }
}

Conclusión

La clave es elegir el pattern correcto para cada caso. En Wegow combinamos varios de estos para tener un código limpio y mantenible.

¿Te ha gustado este artículo?

Si tienes preguntas o quieres discutir sobre estos temas, no dudes en contactarme.

Contáctame
Escríbeme por WhatsApp