Desacoplando la interacción con una API en Vue: vue.$api

09 septiembre 2019
Desacoplando la interacción con una API en Vue: vue.$api

Durante el desarrollo de una web, una SPA en Vue es muy habitual que ese desarrollo implique interactuar con una API y lo habitual será que uses axios para ello (aunque lo que voy a explicar valdría para cualquiera otra librería)

Es muy habitual que cuando necesitamos interactuar con un endpoint de la API, lo hagamos directamente desde el controlador, entendiendo por controlador el componente que se encarga de responder a una ruta.

Por ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
....
data() {
return {
users: []
},
...
methods:
...
getUsers(page) {
axios({
method: 'get',
url: 'https://reqres.in/api/users',
data: {
page
}
}).then(res => {
this.users = res.data.data
}).catch(res => {
// Do something on error
})
}
...

Esto cumple su función básica que es la de pedirle a la API la lista de usuarios de la página indicada y guardarlos en users para que este disponible para renderizar.

Se me ocurren varios problemas que nos podemos encontrar a botepronto:

  • Si necesitamos pedir los usuarios desde varios controladores tendríamos que duplicar este código.
  • Si para hacer la llamada necesitamos añadir alguna cabecera de autorización, token o similar, tendríamos que saber en todas las llamadas a la API que usen esa autentificación aplicarlo y usarlo desde una variable de entorno que deberíamos conocer.
  • La gestión de errores puede ser tediosa, si por ejemplo siempre que la API responda un error lo vamos a mostrar en pantalla de la misma forma tendríamos que programarlo en cada llamada.
  • Lo mismo si usamos loaders en pantalla, tenemos que acordarnos de añadirlos cada vez que usemos una llamada a la API.

Una solución más elegante seria mover las llamadas a la API a funciones en un fichero externo que importamos en cada controlador y de las que hacemos uso:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Api from './api.js'
....
data() {
return {
users: []
},
...
methods:
...
getUsers(page) {
Api.getUsers(page)
.then(res => {
this.users = res.data.data
})
}
...

Y el contenido de api.js sería:

1
2
3
4
5
6
7
8
9
10
11
export default {
getUsers: function (page) {
return axios({
method: 'get',
url: 'https://reqres.in/api/users',
data: {
page
}
})
}
}

Con esto además de simplificar el código ya podríamos reusar la llamada a la API en varios controladores.

Si queremos que cada vez que hagamos una llamada a la API se muestre un cargador en el front (y este desaparezca al terminar la carga), seria razonable usar vuex o similar para esto, entonces nuestro controlador quedaría así

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Api from './api.js'
....
data() {
return {
users: []
},
...
methods:
...
getUsers(page) {
this.$store.commit('loading', true)
Api.getUsers(page)
.then(res => {
this.$store.commit('loading', false)
this.users = res.data.data
})
.catch(error => {
this.$store.commit('loading', false)
})
}
...

(Doy por supuesto que hay un store que tiene una mutación encargada de mostrar un componente loading en la UI)

Pero, de esta forma volvemos a tener el mismo problema, para cada llamada a la función que gestiona la API tenemos que acordarnos de hacer el commit al store tanto al principio de la llamada como cuando devuelve datos como, importante, cuando hay un error.

Vale, llevemos esto a nuestro api.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import store from './store.js' // Donde tengamos nuestra store definida

export default {
getUsers: function (page) {
store.commit('loading', true)
return
axios({
method: 'get',
url: 'https://reqres.in/api/users',
data: {
page
}
})
.then(res => {
store.commit('loading', false)
})
.catch(error => {
store.commit('loading', false)
})
}
}

OJO, pero si dejamos esto así, al capturar la resolución de la promesa (con el then y el catch), nuestro controlador no recibirá dicha resolución, por lo que tenemos que reenviarselas de la siguiente forma:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import store from './store.js' // Donde tengamos nuestra store definida

export default {
getUsers: function (page) {
store.commit('loading', true)
return
axios({
method: 'get',
url: 'https://reqres.in/api/users',
data: {
page
}
})
.then(res => {
store.commit('loading', false)
return res // <--- Devolvemos la misma promesa ya resuelta (propagamos)
})
.catch(error => {
store.commit('loading', false)
throw error // <-- Lanzamos de nuevo la misma excepción
})
}
}

De esta forma el propagar la resolución de la promesa el controlador ni se ha enterado que hemos lanzado el loader y lo hemos desactivado.

Vale, pues ahora, porqué, en lugar de tener que importar api.js en cada controlador, no hacemos que esté disponible en toda la app, de la misma forma que lo esta, por ejemplo la store de vuex: ‘vue.$store’ es decir: ‘vue.$api.getUsers()’

Para ello creamos un plugin de vue, que es tan sencillo como lo siguiente en nuestro api.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import store from './store.js' // Donde tengamos nuestra store definida
import axios from 'axios'

export default {
install (Vue) {
Vue.prototype.$api = Api
}
}
const Api = {
getUsers: function (page) {
store.commit('loading', true)
return
axios({
method: 'get',
url: 'https://reqres.in/api/users',
data: {
page
}
})
.then(res => {
store.commit('loading', false)
return res // <--- Devolvemos la misma promesa ya resuelta (propagamos)
})
.catch(error => {
store.commit('loading', false)
throw error // <-- Lanzamos de nuevo la misma excepción
})
}
}

Y nuestro entrypoint, que suele ser main.js, lo dejamos como algo así:

1
2
3
4
5
6
7
8
9
10
11
import Vue from "vue";
import App from "./App.vue";
import Api from "./api.js"; // <----------------

Vue.config.productionTip = false;

Vue.use(Api) // <----------------

new Vue({
render: h => h(App)
}).$mount("#app");

Ahora ya tenemos disponible el acceso al objecto Api en cualquier controlador con solo usar this.$api.getUser(), y en el caso de nuestro ejemplo quedaría

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
data() {
return {
users: []
},
...
methods:
...
getUsers(page) {
this.$api.getUsers(page)
.then(res => {
this.users = res.data.data
})
}
...

Obviamente nuestro objeto Api puede tener más funciones que hagan llamadas a otros endpoints, tener una forma común de mostrar errores, etc. Eso se lo dejo a vuestra imaginación

Por último, y como spoiler de un próximo post, decir que si hacemos uso de muchos endpoints distintos, el fichero api.js puede ser enorme y lo razonable sería trocearlo, e incluso separar las el objecto de la entidad a la que acceda, por ejemplo this.$api.user.get o this.$api.user.create o this.$api.billing.list o cualquier otro ejemplo que se os ocurra, pero como digo eso da para otro post.

Programador y desarrollador de aplicaciones web, #drupal es mi guia. Amante de la #f1, de las buenas conversaciones y de los pequeños detalles.