Skip to content
En esta página

Usando fetch en Vue

El método fetch() es una de las cosas más útiles que tiene JavaScript. Permite, entre otras cosas, obtener información en tiempo real de una fuente remota en forma asincrónica, es decir, sin bloquear la aplicación. Si fetch() no fuese asincrónico, nuestro frontend quedaría congelado hasta que la información que buscamos llega.

Usando fetch en un e-commerce

Otro uso muy común de fetch() es para obtener la información de productos en un sitio de e-commerce. En un e-commerce los productos nunca están almacenados en el frontend, siempre se obtienen de una fuente exterior (la API del backend).

Pero, ¿por qué la información de productos no se puede guardar en el frontend?

Primero, porque es información que cambia: se agregan productos nuevos, otros son eliminados, otros son modificados. Por ejemplo, el stock de un producto cambia cada vez que se hace una venta. Si la información de productos estuviese en el frontend, cada vez que el stock cambia habría que hacer un nuevo deploy, es decir, volver a subir el sitio entero a un servidor solamente porque el stock de uno de los productos cambió.

Podrían decir que se puede solucionar más fácilmente: cuando el usuario agrega un producto al carrito modifico el stock de ese producto en el array de productos y listo:

js
addToCart(product) {
  this.cart.push(product)
  const updatedProduct = this.products.find(p => p.id === product.id)
  updatedProduct.stock--
}

El problema es que el stock se va a modificar únicamente para un usuario, o sea, el que está agregando el producto al carrito en ese momento. Pero, ¿si al mismo tiempo hay otro usuario usando la aplicación que quiere comprar el mismo producto? ¿Y si ése era el último que quedaba disponible y ya no hay más stock? El segundo usuario nunca se va a enterar de que el producto que está comprando ya no tiene stock.

Es por eso que este tipo de información se maneja en un backend con una base de datos que centraliza la información de productos. Entonces, cuando un usuario compra un producto el frontend hace un fetch() de tipo POST al backend con la información de la compra, y el backend actualiza el stock de los productos comprados.

Y así, cuando el segundo usuario quiere ver los productos disponibles, su browser (o mejor dicho, nuestra aplicación, que está corriendo en su browser) hace un fetch() al backend y obtiene una lista de productos modificada en la que el producto que se quedó sin stock ya no aparece (o está señalado como 'no disponible').

Otra razón por la que los productos no pueden estar almacenados en el frontend es porque la cantidad de productos podría ser enorme. Imagínense si cada vez que entramos a MercadoLibre la información de los cientos de miles de productos que se venden en la plataforma estarían incluídos dentro del frontend. Estaríamos horas esperando hasta poder empezar a usar la aplicación.

El objeto data y fetch

Es por estas razones que, si están creando un e-commerce con Vue, no deberían almacenar la información de productos en el objeto data:

js
export default {

  data: () => ({
    
    // ❌️ Los productos no deberían estar declarados en data ❌️
    productos: [
      {
        id: 1,
        nombre: 'Producto Uno',
        imagen: './assets/imagenes/producto-uno.jpg',
        precio: 100,
        stock: 1
      },
       {
        id: 2,
        nombre: 'Producto Dos',
        imagen: './assets/imagenes/producto-dos.jpg',
        precio: 200,
        stock: 2
      },
       {
        id: 3,
        nombre: 'Producto Tres',
        imagen: './assets/imagenes/producto-tres.jpg',
        precio: 300,
        stock: 3
      }
    ]
  })
}

El objeto data no es una base de datos, es simplemente un objeto reactivo que permite que la vista se actualice de acuerdo a los cambios en la información.

Usando imágenes en un e-commerce

Además, hay otro problema con el ejemplo de acá arriba: al estar guardando las imágenes de los productos dentro de la carpeta assets los paths son relativos (o sea: ./assets/imagenes/producto-uno.jpg) no absolutos (como, por ejemplo: https://dav-leda.github.io/images-bakery/brownies.jpg) entonces los paths se van a romper cuando intenten pasarlos de un componente a otro mediante props.

Por ejemplo, si hacen esto:

html
<template>

  <ProductCard
    v-for="producto in productos" :key="producto.id"
    :producto="producto"
    @add-to-cart="addToCart"
  />

</template>

Y en ProductCard.vue:

html
<template>
  <div class="card">

    <!-- Esta imagen no se va a mostrar 😐️ -->
    <img :src="producto.imagen" :alt="producto.nombre"/>

    <h2>{{ producto.nombre }}</h2>
    <p> $ {{ producto.precio }}</p>

    <footer>
      <button @click="addToCart">Agregar al carrito</button>
    </footer>

  </div>
</template>

<script>

export default {

  props: {
    producto: {
      id: Number,
      nombre: String,
      imagen: String,
      precio: Number,
      stock: Number
    }
  }

  methods: {
    addToCart() {
      this.$emit('add-to-cart', this.producto)
    }
  }
}

Las imágenes de los productos no se van a mostrar porque el path './assets/imagenes/' no es accesible desde el componente ProductCard. Esto es porque este componente está en la carpeta '/components'. Entonces la ruta hacia el path de la imagen es distinta de la que estaba en el componente padre de ProductCard.

Otro problema con guardar las imágenes de productos en el frontend es que las imágenes de los productos pueden cambiar. Por ejemplo, si se agrega un producto nuevo pasaría lo mismo que en el ejemplo del cambio de stock: cada vez que una imagen de un producto es agregada o cambia habría que volver a subir la aplicación entera a un servidor solamente porque una imagen cambió.

Otro problema es que la cantidad de imágenes de productos puede ser muy grande. Por ejemplo, si una plataforma como MercadoLibre guardase los cientos de miles de imágenes de productos en su frontend la aplicación pesaría unos cuantos terabytes...

El único tipo de imágenes que pueden ser guardadas dentro de la carpeta assets del frontend son las imágenes que no cambian o que cambian muy de vez en cuando, como el logo del e-commerce o una imagen de fondo.

Cómo servir imágenes en forma remota

Entonces, ¿dónde guardarlas?

GitHub Pages

Una opción simple es GitHub Pages. Lo único que tienen que hacer es abrir la carpeta que contiene las imágenes con VS Code, crear un repositorio y publicarlo en GitHub. Sería bueno también incluir un index.html que diga <h1>Imágenes</h1> o algo así, para poder tener alguna referencia al acceder al directorio raíz de la página

Luego, en la sección Settings del repositorio van a Pages:

GitHub Pages

Luego seleccionan la branch master y le dan Save. Luego de unos segundos le dan refresh al browser y les va a aparecer un cartel que dice:

GitHub Pages

Your site is live at https://dav-leda.github.io/images-bakery/

Y de ahí pueden acceder a las imágenes con una url absoluta:

https://dav-leda.github.io/images-bakery/pastafrola.jpg

Usando fetch con un backend

Volviendo al ejemplo anterior, ya que la información de productos debe estar en el backend, no en el frontend, ¿cómo se puede acceder a esta información desde el frontend? Usando fetch:

js
const apiUrl = 'https://mi-backend.com/api/productos'

export default {

  data: () => ({
    // Un array vacio
    productos: []
  }),

  // Ejecutar el fetch en created(), es decir, 
  // apenas el objeto data esta diposnible para ser usado
  // pero antes de que el template se renderice
  created() {
    this.fetchProducts(apiUrl)
  },

  methods: {
    async fetchProducts(url) {
      try {
        this.products = await (await fetch(url)).json()
      } catch (err) {
        console.log(err)
      }
    }
  }
}

try/catch

Siempre que hagan llamadas a REST APIs deben ponerlas dentro de un bloque try/catch por si la llamada a la API falla. Recuerden también que el método (en este caso fetchProducts) debe ser asincrónico, ya que fetch devuelve una promesa. Es decir, la respuesta de la API no llega inmediatamente sino que tarda un tiempo. Aunque este tiempo sea muy corto (generalmente es menos de un segundo) la aplicación no puede estar bloqueada mientras la respuesta llega.

Para esto usamos await, y de esta forma la respuesta de la API se guarda en this.products recién cuando está lista. Si no usásemos await en lugar del array de productos tendríamos un mensaje diciendo [object Promise] indicando que la promesa aún no fue resuelta.

Mostrar los console.log sólo en desarrollo

Otra cosa a tener en cuenta es que el console.log con el mensaje de error sólo debe mostrarse mientras estamos desarrollando la aplicación, como una ayuda para debuggear, pero no debería verse cuando la aplicación está en producción, o sea, online.

Si la llamada a la API falla, lo mejor sería mostrarle al usuario un mensaje que pueda entender, como por ejemplo: "Error de conexión, por favor intente nuevamente.". Si el mensaje de error aparece únicamente en la consola, cuando la llamada falla el usuario se quedaría esperando frente a una pantalla en blanco sin entender qué pasó 😕️

Por otro lado, si los console.log se muestran en un sitio que ya está online podrían revelar información interna a cualquiera que entre a las Dev Tools del browser. Y además, da la impresión de una aplicación con errores 🤦‍♂️️

Para que los console.log aparezcan únicamente mientras la aplicación está en desarrollo hay que agregar esto al archivo de configuración de Vite (vite.congig.js):

js
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({

  esbuild: { 
    drop: ['console', 'debugger'], 
  }, 

  plugins: [vue()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

Simulando una API REST

Si aún no tenemos un backend armado, ¿cómo hacemos? En ese caso pueden simular una API REST con un archivo JSON. Este archivo JSON puede ser subido a GitHub Pages de la misma forma que las imágenes: abren una carpeta en VS Code, crean un archivo .json y copian la data de los productos. Asegúrense de que esté en formato JSON, con comillas dobles tanto en los keys como en los valores que sean strings, como en el ejemplo de acá abajo.

Luego crean un repositorio en GitHub con un archivo llamado index.json dentro de la carpeta /products, lo publican, van a Settings, Pages, seleccionan la branch master, le dan save y listo.

json
[
  {
    "id": 1,
    "nombre": "Producto Uno",
    "imagen": "https://username.github.io/imagenes/producto-uno.jpg",
    "precio": 100,
    "stock": 1
  },
  {
    "id": 2,
    "nombre": "Producto Dos",
    "imagen": "https://username.github.io/imagenes/producto-dos.jpg",
    "precio": 200,
    "stock": 2
  },
  {
    "id": 3,
    "nombre": "Producto Tres",
    "imagen": "https://username.github.io/imagenes/producto-tres.jpg",
    "precio": 300,
    "stock": 3
  }
]

Y luego pueden acceder al JSON como si fuese una API REST desde la URL de GitHub Pages:

https://dav-leda.github.io/api/products/

Entonces, en el ejemplo anterior con fetch, pueden reemplazar la variable apiUrl por:

js
const apiUrl = 'https://nombre-de-usuario.github.io/api/products/'

Obviamente, esto sólo les va a servir para peticiones GET, no para POST, PUT o DELETE (o sea, solamente para obtener data, no para agregarla, cambiarla o borrarla desde el frontend). Y tampoco les va a servir para hacer queries para obtener sólo un producto en particular, como esta:

html
<!-- Esto no va a funcionar -->

https://username.github.io/api/products?id=1

Las API REST reales son aplicaciones funcionales, no archivos estáticos, por eso permiten modificar data, o buscarla según los params que le pasemos.

Pero al menos si lo hacen de esta forma, cuando más tarde tengan que usar una API REST real les va a resultar mucho más fácil la transición.

Y ahora ya podemos renderizar nuestras cards de productos con la data proveniente del json en GitHub Pages 🥳️