loading...
Cover image for Architecting HTTP clients in Vue.js applications for efficient network communication
Locale.ai

Architecting HTTP clients in Vue.js applications for efficient network communication

haxzie profile image Musthaq Ahamad Updated on ・8 min read

Modern web apps highly rely on network communication with API servers and external services. From real-time data to static assets, everything is done through the network connection. It is important to design the network interface layer or the HTTP client which helps your application to call the API endpoints to be efficient and robust. In this article we'll discuss ways to design the HTTP clients and making network requests in your Vue.js application, considering some of the best practices and techniques.

We'll look into the following concepts in detail and how to implement them in our application. I prefer using Axios since it gives more flexibility, control, and has an exceptional browser and node.js support.

  1. Creating HTTP Clients using axios instances
  2. Structuring your API endpoints
  3. Making network requests inside Vuex actions
  4. Managing Auth Credentials using interceptors
  5. Handling network errors and logging
  6. Caching and Throttling

Before we start, the code snippets below are written keeping Vue.js developers in mind. But, these can also be used for React or any other frontend library/framework.

This is the second part of the "Architecting Vue application" series. You can find the first part here where I talk about how to Architect Vuex store for large-scale Vue.js applications.


1. Creating HTTP Clients using axios instances

Axios provides out of the box support for having a persistent configuration for all of our API calls using axios instances. We'll be using axios instances as HTTP clients in our application with our configurations. If you are working on a large scale application, it is possible that your application needs to communicate with different API endpoints. In this case, we might need to create multiple axios instances, with its own configuration and separate them out to individual files.

Install axios in your project

$ npm install --save axios

Import axios into your project

Considering best practices, it is recommended to add API URLs into .env files while developing large scale applications. In Vue.js applications, to be able to access the env variables inside your project, we need to prefix it as VUE_APP_. So, if you want to save BASE_URL, create a .env file in the root of your project directory and add the following line.

VUE_APP_BASE_URL=https://myApiServerUrl.com

Once we have our environment variables in place, we can retrieve them while creating axios instances. We can additionally pass all our configuration into this instance, including headers and use this instance to create HTTP requests.

import axios from axios;

const httpClient = axios.create({
    baseURL: process.env.VUE_APP_BASE_URL,
    headers: {
        "Content-Type": "application/json",
        // anything you want to add to the headers
    }
});

export default httpClient;

One more thing to keep in mind, Axios by default has the timeout set to 0, which means no timeout. But in most cases, we need to set request timeouts in our application along with a retry period. We will discuss how to retry a failed request in the below sections but you can change the default timeout of our httpClient while creating it.

const httpClient = axios.create({
    baseURL: process.env.VUE_APP_BASE_URL,
    timeout: 1000, // indicates, 1000ms ie. 1 second
    headers: {
        "Content-Type": "application/json",
    }
});

2. Structuring your API endpoints

As per REST design principles, most of our endpoints might have CURD operations associated with it. So, grouping together the endpoint with all it's request methods is one way to organize your API calls. We can import the required HTTP-client and export all the required requests as methods. Here's an example of grouping all the requests related to Users into a single file.

import httpClient from './httpClient';

const END_POINT = '/users';


const getAllUsers = () => httpClient.get(END_POINT);

// you can pass arguments to use as request parameters/data
const getUser = (user_id) => httpClient.get(END_POINT, { user_id });
// maybe more than one..
const createUser = (username, password) => httpClient.post(END_POINT, { username, password });

export {
    getAllUsers,
    getUser,
    createUser
}

We can follow a simple directory structure for storing all these files.

api/
  ├── httpClient.js  --> HTTP Client with our configs
  ├── users.api.js
  ├── posts.api.js
  └── comments.api.js

And we can use them in our Vue.js components and Vuex store by simply importing them.

import { getAllUsers, getUser } from '@/api/users.api';

3. Making network requests inside Vuex actions

Moving all the business logic into Vuex store, including all of your network requests makes the view components independent. We can use actions in our store to fetch the data and store it in the state object. Vuex actions are synchronous by default, but the only way to know if an action is complete is by making your actions async or returning a promise. We can commit the data to the store through mutations using actions. Here's an example of a store module with actions, which fetches the data and commits to the store.

/*
*   store/modules/users.module.js
*/

// import the api endpoints
import { getAllUsers } from "@/api/users.api"

const state = {
    users: []
}

const getters = {
    getUsers(state) {
        return state.users;
    }
}

const actions = {
    async fetchUsers({ commit }) {
            try {
                const response = await getAllUsers();
                commit('SET_USERS', response.data);
            } catch (error) {
                // handle the error here
            }    
        });
    }
}

const mutations = {
    SET_USERS(state, data) {
        state.users = data;
    }
}

export default {
    namespaced: true,
    state,
    getters,
    actions,
    mutations
}

In our Vue.js component, we can first check the store if there are any data and avoid additional network calls. Or, if there aren't any data, we can use actions to fetch the data.

<template>
    <!-- Your template here -->
</template>

<script>
import { mapActions, mapGetters } from "vuex";

export default {
    data() {
        return {
            isLoading: false;
        }
    },
    computed: {
        ...mapGetters('Users', ['getUsers'])
    },
    methods: {
        ...mapActions('Users', ['fetchUsers'])
    },
    async mounted(): {
        // Make network request if the data is empty
        if ( this.getUsers.length === 0 ) {
            // set loading screen
            this.isLoading = true;
            await this.fetchUsers();
            this.isLoading = false;
        }
    }
}
</script>

4. Managing Auth Credentials using interceptors

Creating interceptors to inject headers is an easy way to secure your requests with Auth credentials. If you are building an application with user login, we can use interceptors to inject Auth token into the headers of each request. In our httpClient.js file we can add the following code to create request interceptors.

import axios from axios;

const httpClient = axios.create({
    baseURL: process.env.VUE_APP_BASE_URL,
    timeout: 5000
});

const getAuthToken = () => localStorage.getItem('token');

const authInterceptor = (config) => {
    config.headers['Authorization'] = getAuthToken();
    return config;
}

httpClient.interceptors.request.use(authInterceptor);

export default httpClient;

5. Handling network errors and logging

Is it easy as response.status === 500 in every request? It's not ideal to check the status and logging these errors in every network request we make inside our actions. Instead, axios offer abilities to intercept the error responses, which is a perfect spot to find errors, log or show a cute notification to the user saying the server effed up. We can also use this to log-out the user from your application if the requests aren't authorized or if the server informs of an expired session.

In the below example, I am using vue-notifications to show tiny notifications on the screen

// interceptor to catch errors
const errorInterceptor = error => {
    // check if it's a server error
    if (!error.response) {
      notify.warn('Network/Server error');
      return Promise.reject(error);
    }

    // all the other error responses
    switch(error.response.status) {
        case 400:
            console.error(error.response.status, error.message);
            notify.warn('Nothing to display','Data Not Found');
            break;

        case 401: // authentication error, logout the user
            notify.warn( 'Please login again', 'Session Expired');
            localStorage.removeItem('token');
            router.push('/auth');
            break;

        default:
            console.error(error.response.status, error.message);
            notify.error('Server Error');

    }
    return Promise.reject(error);
}

// Interceptor for responses
const responseInterceptor = response => {
    switch(response.status) {
        case 200: 
            // yay!
            break;
        // any other cases
        default:
            // default case
    }

    return response;
}

httpClient.interceptors.response.use(responseInterceptor, errorInterceptor);


6. Caching and Throttling

Axios adapters provide abilities to add superpowers into your HttpClient. Custom adapters are a clean way to enhance network communication in your application using caching and throttling. We'll be using axios-extensions to attach caching and throttling adapters to our httpClient.

Note that caching from client side is not recommended because your server has more knowledge on when the data changes. As @darthvitalus pointed out, it is better to set the cache headers to tell the browser what caching strategy to use. You can follow the below examples, if you still want to use caching from the client side.

Install axios-extensions

$ npm install --save axios-extensions

Caching

import axios from 'axios';
import { cacheAdapterEnhancer } from 'axios-extensions';

const cacheConfig = {
    enabledByDefault: false, 
    cacheFlag: 'useCache'
}

const httpClient = axios.create({
    baseURL: process.env.VUE_APP_BASE_URL,
    headers: {
        'Cache-Control': 'no-cache'
    },
    adapter: cacheAdapterEnhancer(axios.defaults.adapter, cacheConfig);
})

Once we have set up the cache adapter, we can config each request to be cached after it's first request. In our file, where we defined the end points we can pass an additional parameter indicating that the response should be cached.

const getUsers = () => httpClient.get('/users', { useCahe: true });

All the subsequent calls after the first call will be responded from the cache.

getUsers(); // actual network request and response gets cached
getUsers(); // from cache
getUsers(); // from cache

Throttling

In our use case, throttling means limiting the number of requests made in a particular amount of time. In large scale applications where each request to the server amounts to a larger cost of computing, caching is one way to achieve throttling.

What if there is new data coming in every once and then? In that case, we can use throttling to respond from cache for a limited time and then make an actual request after the specified time period. Axios-extensions comes with a throttleAdapterEnhancer which can be used to throttle the network request in our application. If we are using throttling, we can avoid using a persistent cache.

keep in mind it is not recommended to use throttling for time-sensitive data. If your data changes quite often, your server is the only entity that knows about the data. Use cache headers instead to let the browser know about what chaching strategy to use.

import axios from 'axios';
import { throttleAdapterEnhancer } from 'axios-extensions';

const throttleConfig = {
    threshold: 2*1000 // 2 seconds
}

const httpClient = axios.create({
    baseURL: process.env.VUE_APP_BASE_URL,
    adapter: throttleAdapterEnhancer(axios.defaults.adapter, throttleConfig)
});

export default httpClient;

If we have set up throttling, Same requests made within the threshold period will be responded from the cache. Only real request is made after the threshold period.

getUsers(); // actual request
getUsers(); // responds from cache
getUsers(); // responds from cache


setTimeout(() => {
    getUsers(); // threshold period passed, actual request.
}, 2*1000);

Thanks for reading this article 💖. Liked the article? have some feedback or suggestions? leave a like and a comment. This will help me understand better and write more amazing articles for you 🙂.

What's next?

In my upcoming posts, we'll discuss more Architecting large scale Vue.js applications in terms of performance and your productivity.


Hi there! I work as a UX Engineer at Locale.ai solving Geo-Spatial problems for our B2B customers. If you think you love solving UX problems for users, love designing and want to work with a team of enthusiastic individuals, check out the job openings we have at Locale.
Wanna talk? You can find me on Twitter, Instagram and GitHub.

Originally posted on haxzie.com

Posted on by:

haxzie profile

Musthaq Ahamad

@haxzie

Building locale.ai • UX Engineer • I write, design and build for people of internet.

Locale.ai

We are a team working on a mission to make location based insights an integral part of your everyday decisions.

Discussion

markdown guide
 

in the axios intance is baseURL not baseUrl


const httpClient = axios.create({
    baseURL: process.env.VUE_APP_BASE_URL,
    headers: {
        "Content-Type": "application/json",
        // anything you want to add to the headers
    }
});

Thanks!

 

Thanks for letting me know 😉✨

 

If you could update the post, that'd be great! I was banging my head for a bit until I figured out the typo :)

 

Hi,

Can you explain to me this mapping? My first thought was the getUsers is the function, but it is an array instead?

    computed: {
        ...mapGetters('Users', ['getUsers'])
    },
    methods: {
        ...mapActions('Users', ['fetchUsers'])
    },
    mounted: {
        // Make network request if the data is empty
        if ( this.getUsers.length === 0 ) {
            // set loading screen
            this.isLoading = true;

            this.fetchUsers().then(() => {
                this.isLoading = false;
            }).catch(err => {
                // show error screeen
            });
        }
    }
 

It's the getter defined in the store. Getters acts as computed properties so they're not called like a function.

 

I know how a simple getter works, you pass in an array and it maps the getters. In your example, there is also the 'Users' before the array element.

When we create modules in a Vuex store, that is; you split your store into smaller stores. It's a good idea to namespace them. Eg, if you have an application that has a store, you can create modules inside it for User data, Post data and so on. When you have modules, you need to pass the module name (or the namespace) to get the getter/action from that module to mapping functions. You can read more about this from my previous Article "Architecting Vuex store for large scale Vue.js application".

In simple words, if you want to access the methods in a store module namespaced as Users, you would use Users/getUsers to access the getter. Instead of that, you can simply pass the module name as the first argument for mapGetters/mapActions to access methods from that module as I have used in the example :)

mapGetters(['Users/getUsers'])

is same as

mapGetters('Users', ['getUsers'])

Now it's clear, I used the mapGetters(['Users/getUsers']) notation for modules, not the other one

Awesome! Hope this helped you and hope you learned something handy :)

 

Hi, fellow! Thanks for such a valuable post! Saved it for future ;)

I've some questions though)

  1. Making network requests inside Vuex actions

Isn't it better to use async/await for clearer and more declarative code?
Some code above could be written like this:

const actions = {
    fetchUsers: async function ({ commit }) {
        const response = await getAllUsers();
        commit('SET_USERS', response.data);
    }
}

sure it needs try/catch around ;)

  1. Managing Auth Credentials using interceptors

Saving tokens or some other credentials/auth data to LS is bad, as spoken clearly here:
https://dev.to/rdegges/please-stop-using-local-storage-1i04
Do you still use this LS approach? Or you just used it as an easy example?

  1. Caching and Throttling

About this lines and idea:
"All the subsequent calls after the first call will be responded from the cache.

getUsers(); // actual network request and response gets cached
getUsers(); // from cache
getUsers(); // from cache...

"

What if in between these requests server's data has actually changed? Thus client will get wrong (cached) data. I believe caching is more server stuff to do, via response headers, telling browser what caching strategy to use, because server does better knowledge about information it stores, and about ways and frequency of data changes. So why not do caching on server-browser?

Thanks in advance!)

 

Great Observation! Thanks for the suggestions, I have updated the article with your feedback. And for the part where I used Local Storage, it's just for an easy example, adding to that using cookies instead of localStorage doesn't make much difference since both use the same security policy. Totally agree with implementing robust security in place without using localStorage but If your website is vulnerable to XSS, both the cases should be deemed to be flawed. Storing JWT in localStorage can be made more secure by issuing short term tokens. But yeah, choosing security and ease of implementation is always a matter of trade-offs between both. Thanks for your suggestions, hope it will help the readers alot! :)

 

Thank you for share it with us !
Great approach/practices about Vue component, making more clear code and scalable.

Vue and Nuxt are great frameworks I like a lot.

I hope that you continues to sharing more content like this. I'm excited to see your next post.

 

I signed up to DEV.to just to thank you for this article. Thank you.

 
 
 

Nice article!
Actually, this applies also to React.

 
 
 
 
 

Your error handling does not work in the event of a network error (server offline) as it does not have a response object (error.response)

 

Hey, thanks for letting me know :)
This is a fairly simple fix to just check if error.response is present before switch-casing the error.response.status.

Will update the post with these changes.
Thank you.