vue.js, axios, interceptors and toast-notifications

vue.js, axios, interceptors and toast-notifications

I usually use axios when it comes to Asynchronous JavaScript and XML (ajax) or more modern Asynchronous JavaScript and JSON (ajaj). I like that axios runs in the browser and in node.js, and with a promise polyfill in IE11 as well - and projects I usually do, do support IE11.

In my recent projects, I wanted to add toast-notifications, these little jump-in-notifications, telling my visitor that the app is doing something or something went wrong.

Besides notifications like "you're offline / you're online / your connection speed changed" I wanted to inform my visitor of api calls with toasts. Not every api call, not instantly, but sometimes. And this is where axios interceptors really come in handy.

A full working example of this blog post is available here: vue-axios-interceptor.

project setup

Starting point is a vue.js project setup with @vue/cli and the vue-axios extension, and vue-toastification (the toasting comes later in that post):

// /src/main.js
import Vue from 'vue';
import VueAxios from 'vue-axios';
import axios from 'axios'; // this is all about to change
…
import App from './App.vue';
…
Vue.use(VueAxios, axios);
…
new Vue({
  …
}).$mount('#app');

Vue-axios takes an axios instance and makes it available everywhere in your app, so you can call this.axios.get/post/delete/update or this.$http… at any time in your app.

custom axios interceptor configuration

Axios interceptors can setup some request and response handling globally in your app. In my example I use interceptors to measure the time an api call takes, and to show a toast "fetching data" automatically, so I do not need to add this function on every API call. In a component or view you type this.axios.get(...) and the interceptor will do their work.

First create a src/plugins/axios.js file and configure your interceptors and more:

// /src/plugins/axios.js
import axios from 'axios';

// doing something with the request
axios.interceptors.request.use(
  (request) => {
    // do something with request meta data, configuration, etc
    …
    // dont forget to return request object,
    // otherwise your app will get no answer
    return request;
  }
);

// doing something with the response
axios.interceptors.response.use(
  (response) => {
     // all 2xx/3xx responses will end here
     
     return response;
  },
  (error) => {
     // all 4xx/5xx responses will end here
     
     return Promise.reject(error);
  }
);

export default axios;

I hope you see the power axios lays in your hands here.

exchange some data between request and response

For a bit we stay in that clear interceptor context, because you can add data in the request object, which will end up in your response object.
For example a timestamp if you want to measure the speed of your api, or something else you create on request start and want to reuse on response:

// /src/plugins/axios.js
import axios from 'axios';

axios.interceptors.request.use(
  (request) => {
    // eslint-disable-next-line no-param-reassign
    request.config = {
      ...(request.config ?? {}), // preserve a given request.config object
      start: Date.now(),
    };

    return request;
  },
);

axios.interceptors.response.use(
  (response) => {
    const now = Date.now();
    console.info(`Api Call ${response.config.url} took ${now - response.config.config.start}ms`);
    return response;
  },
  (error) => Promise.reject(error),
);

export default axios;

use custom axios instance

Instead of importing "native" axios, we import our plugins/axios instance and use this with vue-axios:

// /src/main.js
import Vue from 'vue';
import VueAxios from 'vue-axios';
…
import axios from './plugins/axios';
…
Vue.use(VueAxios, axios);
…

App Axios Toasts

With that setup, we can now use toast messages to inform our users when an api call is started. I will use vue-toastification for toasting:

// /src/main.js
import Vue from 'vue';
import VueAxios from 'vue-axios';
import Toast from 'vue-toastification';
import axios from './plugins/axios';
import App from './App.vue';

import 'vue-toastification/dist/index.css';

Vue.config.productionTip = false;

Vue.use(VueAxios, axios);
Vue.use(Toast);

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

tell axios to show or not to show a toast

By starting an axios call in our app, we will provide some config data, so our axios interceptor can decide if a toast is needed or not:

// some component.vue
export default {
…
  methods: {
    loadData() {
      this.axios.get(
        `/route/to/my/api/${this.$route.param.id}`,
        {
        config: {
          showToast: true,
          requestToast: {
            title: 'Loading Data',
          },
          responseToast: {
            title: 'Data loaded!',
          },
        })
        .then(({ data }) => {
          this.data = data;
        })
        .catch((error) => {
          console.error('api call failed', error);
        });
    },
  },
  …
};

Let's look at our axios interceptor file:

// /src/plugins/axios.js
import Vue from 'vue';
import axios from 'axios';

axios.interceptors.request.use(
  (request) => {
    // eslint-disable-next-line no-param-reassign
    request.config = {
      showToast: false, // may be overwritten in next line
      ...(request.config || {}),
      start: Date.now(),
    };

    if (request.config.showToast) {
      // eslint-disable-next-line no-param-reassign
      request.config.requestToastId = Vue.$toast(
        request.config.requestToast.title,
      );
    }

    return request;
  },
);

axios.interceptors.response.use(
  (response) => {
    const now = Date.now();
    const request = response.config;
    console.info(`Api Call ${request.url} took ${now - request.config.start}ms`);

    if (request.config.requestToastId) {
      Vue.$toast.dismiss(request.config.requestToastId);
    }

    if (request.config.showToast && request.config.responseToast) {
      Vue.$toast(request.config.responseToast.title);
    }
    return response;
  },
  (error) => Promise.reject(error),
);

export default axios;

So - we keep the call measurement and if a toast is configured, we show that toast, keep the toast id, so we can dismiss that toast on response and show a success toast to our users.

In the last step, I imported the Vue instance to the axios file, so I have access to the $toast component.  Another usecase could be to handle unauthorized api calls, or other errors within an axios call.

Have a look at my example vue-axios-interceptor project with a full working app, consuming the dog api 🐕.

Article Image from Chris Ovalle via unsplash and ghost ♥.