Consumiendo una API desde Vue.JS

Aprovechando el día de fiesta y que en casa todos duermen todavía, voy a retomar los post más técnicos.

En esta ocasión volveré sobre un tema que ya inicié hace unos días, el acceso a una API desde PHP, aunque en este ejemplo que vamos a ver, el acceso se hará consumiendo esa misma API desde Vue.js.

Para trabajar este ejemplo, vamos usar la misma API que se configuró, un listado de películas y accederemos a ella, validando un usuario que nos traiga un par de tokens que nos permitan ver los datos.

Empezamos

Lo primero será configurar un proyecto vue.js básico, que con pocas modificaciones, nos permita preparar el acceso. Creamos desde la terminal el proyecto vue:

vue create vue_movies

Elegimos la seleccion manual, para poder configurar el proyecto a nuestro gusto

En el caso que nos ocupa, y para hacerlo lo más sencillo posible, seleccionamos Babel y Router. Este último nos permitirá montar rutas y poder navegar por las páginas del proyecto:

Seleccionadas esas opciones, solo queda seguir con la configuración por defecto y dejar que el proceso termine. Cuando haya finalizado, arrancaremos el proyecto con

yarn serve

y tendremos acceso a la aplicación a través de la ip que nos indica la salida

Configurando

Una vez correctamente instalado el proyecto vue, es necesario importar Axios, que es la libería que nos permitirá gestionar adecuadamente las llamadas a la API. Esto se consigue simplemente con

yarn add axios

Una vez terminado el proceso de instalación, podremos empezar a tocar código.

Como personalmente me gusta tener los ficheros muy organizados, voy a crear en la carpeta src, otra carpeta llamada logic, donde almacenaré toda la lógica del acceso a la API y los datos de configuración. Esto es algo muy personal, así que cada uno puede organizase como quiere.

Lo primero que voy a crear es un fichero al que llamaré config.js, donde incluiré todos los datos de conexión necesarios y las direcciones de los distintos endpoints que vamos a usar. Quedará algo así:

// Configuraciones y endpoints
// config.js

// datos del usuario
const username = '_NOMBRE_USUARIO_';
const password = '__PASSWORD__';

// Endpoints
// ACCESO A LA AUTENTICACIÓN
const API_AUTH = 'http://127.0.0.1:8000'; 

// ACCESO A LOS DATOS DE LAS PELICULAS
const API_MOVIES = 'http://127.0.0.1:8000/movies/movies/';

// EXPORTAMOS LOS PARAMETROS PARA QUE ESTÉN DISPONIBLES
export { username, password, API_AUTH, API_MOVIES };

A continuación, creo un fichero llamado auth.js, que contendrá toda la lógica de conexión a la API y si esta es correcta, nos devolverá los token access y token refresh, necesarios para las validaciones del resto de llamadas.

// VALIDACIÓN DEL USUARIO
// auth.js

import axios from 'axios'; // llamamos a Axios
import { API_AUTH, username, password } from './config'; // llamamos a la configuración del usuario/password

// Función para autenticar y obtener tokens
async function authenticateAndGetTokens() {
  try {
    const response = await axios.post(`${API_AUTH}/users/token/`, {
      username,
      password,
    });
    const { access, refresh } = response.data;

    // Guardar tokens en el almacenamiento local (localStorage)
    localStorage.setItem('access_token', access);
    localStorage.setItem('refresh_token', refresh);

    return { access, refresh };
  } catch (error) {
    console.error('Error en la autenticación:', error);
    return null;
  }
}

// Función para refrescar el token de acceso
async function refreshAccessToken() {
  const refresh = localStorage.getItem('refresh_token');

  if (refresh) {
    try {
      const response = await axios.post(`${API_AUTH}/users/token/refresh/`, { refresh });
      const newAccessToken = response.data.access;

      // Actualizar el token de acceso en el almacenamiento local
      localStorage.setItem('access_token', newAccessToken);

      return newAccessToken;
    } catch (error) {
      console.error('Error al refrescar el token de acceso:', error);
      return null;
    }
  } else {
    return null;
  }
}

// Función para obtener el token de acceso
function getAccessToken() {
  return localStorage.getItem('access_token');
}

// Exportamos los tokens para otras llamadas posteriores
export { authenticateAndGetTokens, refreshAccessToken, getAccessToken };

Si la conexión es correcta, podremos ver en la consola, la salida con los diferentes tokens:

Terminada la configuración de la conexión del usuario, llega el momento de hacer la llamada a la API de películas. Recordemos que tal como se configuró originalmente, esta espera que el usuario esté autenticado para poder devolver los datos (ver configuración de la API aquí). Recordamos la vista, tal como estaba configurada:

from rest_framework import generics
from .models import Movie
from .serializer import MovieSerializer
from rest_framework.permissions import IsAuthenticated

class MovieList(generics.ListCreateAPIView):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    // La vista espera a un usuario autenticado
    permission_classes = [IsAuthenticated]

class MovieDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    permission_classes = [IsAuthenticated]

Ahora pues, recordado esto, llega el momento de crear un fichero que nos permita conectar a esa lista de datos. Le llamaremos api_movies.js y tendrá la siguiente configuración:

// LLAMADA A LA API DE LAS PELÍCULAS
// api_moves.js

import axios from 'axios';
import { getAccessToken } from './auth'; // Importa la función para obtener el token de acceso
import { API_MOVIES } from './config'; // Importamos el endpoint de las películas desde config.js

// Obtener la lista de películas
async function getMovies() {
  try {
    const token = getAccessToken(); // Obtiene el token de acceso

    if (token) {
      const response = await axios.get(`${API_MOVIES}`, {
        headers: {
          Authorization: 'Bearer ' + token,
        },
      });
      return response.data;
    } else {
      console.error('Token de acceso no disponible');
      return null;
    }
  } catch (error) {
    console.error('Error al obtener películas:', error);
    return null;
  }
}

// Obtener detalles de una película por ID
async function getMovieById(movieId) {
  try {
    const token = getAccessToken(); // Obtiene el token de acceso

    if (token) {
      const response = await axios.get(`${API_MOVIES}/movies/${movieId}/`, {
        headers: {
          Authorization: 'Bearer ' + token,
        },
      });
      return response.data;
    } else {
      console.error('Token de acceso no disponible');
      return null;
    }
  } catch (error) {
    console.error('Error al obtener detalles de la película:', error);
    return null;
  }
}

// Exportamos los datos para poder usarlos en otros sitios
export { getMovies, getMovieById };

Configurada la llamada a la API, ahora necesitamos crear una vista para poder llamarla. Llega el momento de trabajar con los componentes de Vue.js. Vamos a crear una vista dentro de la carpeta views, que llamaremos Movies.vue.

De momento vamos a crear una vista vacia, solamente para comprobar que las rutas, que hemos de configurar en router/index.js funcionan correctamente:

// Vista para las películas
// Movies.vue

<template>
  <div>Vista para las películas</div>
</template>

<script>
export default {

}
</script>

<style>

</style>
// Fichero con las rutas
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/about',
    name: 'about',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
  },
  // Creamos una ruta nueva para las peliculas que llamaremos movies
  // siguiendo el ejemplo de la ruta 'about'
  {
    path: '/movies',
    name: 'movies',
    component: () => import(/* webpackChunkName: "movies" */ '../views/Movies.vue')
  },
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

Ahora en App.vue, el fichero principal de la aplicación, creaemos el enlace a la nueva vista, dentro de <template></template>:

<template>
  <nav>
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link> |
   <!--Añadimos la nueva ruta -->
    <router-link to="/movies">Movies</router-link>
  </nav>
  <router-view/>
</template>

Si la configuración ha sido correcta, podremos ver en nuestra aplicación base, un nuevo enlace y su contenido:

Accediendo a las películas

Ahora que hemos comprobado que la ruta funciona correctamente, vamos configurar la vista para que muestre los datos de las películas.

Primero, probaremos una configuración básica, que nos mostrará en la consola, si todo está OK, el array que hemos traído con los datos de las películas. Para ellos modificamos el fichero Movies.vue de esta manera:

<template>
  <div>
    <!-- Aquí se mostrarian los datos en pantalla -->
  </div>
</template>

<script>
// Importamos las configuraciones y funciones que hemos visto antes
import { authenticateAndGetTokens } from '@/logic/auth';
import { username, password } from '@/logic/config';
import { getMovies, getMovieById } from '@/logic/api_movies'; 

export default {
  async mounted() {
    // Realiza la autenticación y obtiene los tokens al cargar el componente
    const tokens = await authenticateAndGetTokens(username, password);
    if (tokens) {
      console.log('Autenticación exitosa:', tokens);
      // Llama a las funciones de API después de la autenticación exitosa
      this.getMoviesAndDetails(tokens.access_token);
    } else {
      console.log('Error en la autenticación');
      // Maneja el error de autenticación aquí
    }
  },
  methods: {
    async getMoviesAndDetails(accessToken) {
      // Llamada para obtener la lista de películas
      const movies = await getMovies(accessToken);
      if (movies) {
        console.log('Lista de películas:', movies);

      } else {
        console.error('Error al obtener la lista de películas');
        // Maneja el error de obtención de películas aquí
      }
    },
  },
};
</script>

Si la conexión es correcta, podremos ver en consola, el array de las películas importadas:

Comprobado que hemos traido correctamente los datos, vamos a darle un poco de formato para poder mostrarlos en pantalla, que al fin y al cabo es como lo verán los usuarios. Para ello, modificaremos el fichero Movies.vue, para que los datos se muestren en forma de tabla:

<template>
  <div>
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>Título</th>
          <th>Género</th>
          <th>Año</th>
          <th>Director</th>
          <!-- Agrega aquí más encabezados si hay más detalles -->
        </tr>
      </thead>
      <tbody>
        <tr v-for="movie in movies" :key="movie.id">
          <td>{{ movie.id }}</td>
          <td>{{ movie.title_movie }}</td>
          <td>{{ movie.gendre_movie }}</td>
          <td>{{ movie.year_movie }}</td>
          <td>{{ movie.director_movie }}</td>
          <!-- Muestra más detalles si es necesario -->
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
// Importamos las configuraciones y funciones que hemos visto antes
import { authenticateAndGetTokens } from '@/logic/auth';
import { username, password } from '@/logic/config';
import { getMovies, getMovieById } from '@/logic/api_movies'; // Importa las funciones del archivo api_movies

export default {
  data() {
    return {
      movies: [] // Inicializa la lista de películas como un array vacío
    };
  },
  async mounted() {
    // Realiza la autenticación y obtiene los tokens al cargar el componente
    const tokens = await authenticateAndGetTokens(username, password);
    if (tokens) {
      console.log('Autenticación exitosa:', tokens);
      // Llama a las funciones de API después de la autenticación exitosa
      this.getMoviesAndDetails(tokens.access_token);
    } else {
      console.log('Error en la autenticación');
      // Maneja el error de autenticación aquí
    }
  },
  methods: {
    async getMoviesAndDetails(accessToken) {
      // Llamada para obtener la lista de películas
      const movies = await getMovies(accessToken);
      if (movies) {
        console.log('Lista de películas:', movies);
        this.movies = movies; // Asigna las películas al array movies para mostrar en la tabla
      } else {
        console.error('Error al obtener la lista de películas');
        // Maneja el error de obtención de películas aquí
      }
    },
  },
};
</script>

De esta forma, tendremos los datos en pantalla, en un tabla, algo más comprensibles para los usuarios:

Formateando y paginando

En este punto, el proyecto devuelve los datos correctamente y los muestra al usuario por pantalla. Pero es cierto que la visualización no es la más optima posible, y el mostrar todo el paquete de datos completo, de una sola vez, hace que sea complicado verlos adecuadamente.

Vamos por tanto ahora, a personalizar la salida un poco, usando Bootstrap, para ver la tabla de forma más agradable y finalmente paginaremos los datos para poder navegar por ellos.

Lo primero que haremos será incluir Bootstrap, como libreria css en nuestro proyecto. Eso podemos hacerlo directamente incluyendo la llamada al CDN en el fichero public/index.html de esta manera:

<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <!-- Incluimos Bootstrap -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
    <!-- Incluimos Bootstrap -->
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

Una vez hecho esto, modificaremos el template de nuestra vista, para que incluya las clases propias de Bootstrap:

<table class="table">
            <!-- Encabezados de la tabla -->
            <thead>
              <tr>
                <th>ID</th>
                <th>Título</th>
                <th>Género</th>
                <th>Año</th>
                <th>Director</th>
                <!-- Agrega aquí más encabezados si hay más detalles -->
              </tr>
            </thead>
            <tbody>
              <!-- Mostrar películas según la página actual -->
              <tr v-for="(movie, index) in paginatedMovies" :key="index">
                <td>{{ movie.id }}</td>
                <td>{{ movie.title_movie }}</td>
                <td>{{ movie.gender_movie }}</td>
                <td>{{ movie.year_movie }}</td>
                <td>{{ movie.director_movie }}</td>
                <!-- Muestra más detalles si es necesario -->
              </tr>
            </tbody>
          </table>

De esta forma, la visualización de los datos será mucho más agradable:

De todas formas, sigue cargando todo el bloque de datos entero y eso se hace muy incomodo para poder verlos. Así que vamos a añadir una pequeña paginación que nos mostará 10 peliculas por página. Existen librerias de Vue.js que hacen esto mismo, pero en este caso, vamos hacerlo de forma directa y sin usar elementos externos. Además como extra, vamos a incluir un mensaje en pantalla si la conexión por algún motivo no ha sido OK, para infomar al usuario.

Necesitamos modificar el fichero Movies.vue de esta manera:

<template>
  <div class="container">
    <div class="row justify-content-center">
      <div class="col-md-10">
        <div v-if="errorMessage" class="alert alert-danger" role="alert">
          {{ errorMessage }}
        </div>
        <div v-else>
          <table class="table">
            <!-- Encabezados de la tabla -->
            <thead>
              <tr>
                <th>ID</th>
                <th>Título</th>
                <th>Género</th>
                <th>Año</th>
                <th>Director</th>
                <!-- Agrega aquí más encabezados si hay más detalles -->
              </tr>
            </thead>
            <tbody>
              <!-- Mostrar películas según la página actual -->
              <tr v-for="(movie, index) in paginatedMovies" :key="index">
                <td>{{ movie.id }}</td>
                <td>{{ movie.title_movie }}</td>
                <td>{{ movie.gender_movie }}</td>
                <td>{{ movie.year_movie }}</td>
                <td>{{ movie.director_movie }}</td>
                <!-- Muestra más detalles si es necesario -->
              </tr>
            </tbody>
          </table>

          <!-- Botones de paginación -->
          <div>
            <span>Página {{ currentPage }}</span>
          </div>
          <div >
            <button class="btn btn-primary" style="margin-left: 20px;" @click="previousPage" :disabled="currentPage === 1">Anterior</button>
            <button class="btn btn-secondary" @click="nextPage" :disabled="isLastPage">Siguiente</button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { authenticateAndGetTokens } from '@/logic/auth';
import { username, password } from '@/logic/config';
import { getMovies, getMovieById } from '@/logic/api_movies'; // Importa las funciones del archivo api_movies

export default {
  data() {
    return {
      movies: [], // Inicializa la lista de películas como un array vacío
      errorMessage: '', // Variable para almacenar mensajes de error
      currentPage: 1, // Página actual
      itemsPerPage: 10 // Cantidad de películas por página
    };
  },
  computed: {
    paginatedMovies() {
      // Cálculo de películas por página basado en la página actual
      const startIndex = (this.currentPage - 1) * this.itemsPerPage;
      const endIndex = startIndex + this.itemsPerPage;
      return this.movies.slice(startIndex, endIndex);
    },
    isLastPage() {
      return this.currentPage === Math.ceil(this.movies.length / this.itemsPerPage);
    }
  },
  async mounted() {
    try {
      const tokens = await authenticateAndGetTokens(username, password);
      if (tokens) {
        console.log('Autenticación exitosa:', tokens);
        this.getMoviesAndDetails(tokens.access_token);
      } else {
        this.errorMessage = 'Error en la autenticación';
      }
    } catch (error) {
      console.error('Error al conectarse a la API:', error);
      this.errorMessage = 'Error al conectar con la API';
    }
  },
  methods: {
    async getMoviesAndDetails(accessToken) {
      try {
        const movies = await getMovies(accessToken);
        if (movies) {
          console.log('Lista de películas:', movies);
          this.movies = movies;
        } else {
          this.errorMessage = 'Error al obtener la lista de películas';
        }
      } catch (error) {
        console.error('Error al obtener películas:', error);
        this.errorMessage = 'Error al obtener películas desde la API';
      }
    },
    previousPage() {
      if (this.currentPage > 1) {
        this.currentPage--;
      }
    },
    nextPage() {
      if (!this.isLastPage) {
        this.currentPage++;
      }
    }
  }
};
</script>
<style >
.btn {margin-right: 10px;}
</style>

Modificado de esta manera, podremos obtener diferentes mensajes de error, tanto si falló la conexión con la API de las películas

como si falló la conexión del usuario

El resultado de la paginación se puede ver aquí

Y esto es el final del post. Como veís, hemos aprendido a consumir una API con Vue.js y mostrarla de forma correcta al usuario.

En próximas entradas, la idea es seguir trabajando con este proyecto y añadir algunas operaciones más como un CRUD y un buscador, pero eso, para otra ocasión.

Os dejo en mi repositorio de GitHub, los ficheros de este proyecto.

Por Jose Manuel Sanz Prieto

Desarrollador web. En este blog hablo de fotografía, programación con Django, Python, PHP y privacidad.

1 comentario

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *