Fabio Vedovelli

At this point of my life, purely Javascript!

Front End Service Layer with Axios

February 27, 2020

In this article I’m gonna show you how I keep my AJAX calls organized in services which in turn are consummed by the Vuex store.

Disclaimer: Although the article uses Vue.js for demonstration, this technique perfectly suits a React project with Redux or a Svelte project.


The source code of this article is waiting for you in this repository: https://github.com/vedovelli/article-service-layer

Service Layer

Picture by unsplash-logoGalen Crout

When I started learning how to develop complex and interactive interfaces I used to keep everything in the component. The component needs a list of objects? Import Axios, make the AJAX call, and store the data returned in the local state of the component. The request returns an error? It is also the component’s responsibility to deal with it.

This flow worked well for a while but soon I’ve bumped into the code duplication problem. Why dealing with errors coming from the server over and over again if I know it is possible to do it centrally? I also faced the limitation of sharing information that had already been requested by other the component(s).

After a time of research and experimentation, I arrived at the model I call Service Layer.

Understanding the Service Layer

The complete flow Download larger image

It consists of creating a services folder in your src folder and then files with methods to make the AJAX calls. These methods will always return a Promise, allowing components to take actions after its resolution, if necessary.

Folder structure

src/services/index.js

import axios from 'axios';
import * as userService from './users';

/*
 * You can customize Axios settings
 * Ver mais em https://github.com/axios/axios
 */
const http = axios.create({
  baseURL: '/api',
});

// Exports Axios object to be used by the services
export default http;

// Exports the services so they can be used by Vuex's actions
export { userService };

src/services/users.js

import http from '@/service';

export const users = () => http.get('users');

Moving forward with good practices, services are used EXCLUSIVELY by the Vuex store, which uses the data returned to supply its state and thus make the data available to all components that are interested in.

The flow summary is:

Component (executes the Action) > Action (executes one or more Service methods) > Service (makes the AJAX call) > Action (receives back the data and stores it in the Vuex state).

What about the error messages?

In Service Layer I add interceptors (I use Axios) that allow me to interact with calls before and after their execution. For the moment BEFORE I can customize the request by adding new elements, such as a header with JWT. If I need to act AFTER a response is received, for instance, to check if an error has occured in the server, I execute a Vuex action that will save the error message in the Vuex store. The message will then be available to all components that are interest in it.

src/store/index.js

import Vue from 'vue';
import Vuex from 'vuex';

import { userService } from '@/service';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    users: [],
    ui: {
      error: '',
    },
  },
  mutations: {
    ['SET_USERS'](state, { users }) {
      state.users = users;
    },
    ['SET_ERROR_MESSAGE'](state, { error }) {
      state.ui.error = error;
    },
  },
  actions: {
    async fetchUsers({ commit }) {
      const {
        data: { users },
      } = await userService.users(); // <<< uses the Service Layer to call the API

      commit('SET_USERS', { users });
    },
    setErrorMessage({ commit }, { error }) {
      commit('SET_ERROR_MESSAGE', { error });
    },
  },
});

And then in the component:

import { mapActions, mapState } from 'vuex';

const methods = mapActions(['fetchUsers']);

const computed = mapState(['users']);

export default {
  name: 'ServiceLayer',
  computed,
  methods,
  mounted() {
    this.fetchUsers();
  },
};

What else do I do in Service Layer?

Let’s say I need to format a data payload before sending to the server. This is done in service layer. Something very important: sanitize the data before sending to the server. I do it there too (it can be done method by method or in the Axios interceptor). Or I need to parse the data bafore storing it, making it easier for the components to work with it. This is also done by Service Layer.

src/services/index.js

import axios from 'axios';
import store from '@/store';
import * as userService from './users';

/*
 * You can customize Axios settings
 * Ver mais em https://github.com/axios/axios
 */
const http = axios.create({
  baseURL: '/api',
});

http.interceptors.request.use(
  config => {
    /*
     * Here you can add a header with a JWT token, ensuring it will be
     * sent with ALL your requests.
     */
    return config;
  },
  error => Promise.reject(error),
);

http.interceptors.response.use(
  response => response,
  error => {
    /*
     * Here you can add a central error handling mechanism
     */
    store.dispatch('setErrorMessage', { error: error.response.data });

    return Promise.reject(error);
  },
);

// Exports Axios object to be used by the services
export default http;

// Exports the services so they can be used by Vuex's actions
export { userService };

src/services/users.js

import xss from 'xss';
import { cloneDeep } from 'lodash';
import http from '@/service';

export const users = () => http.get('users');

export const postUser = data => {
  /*
   * This is a good spot to sanitize data before POSTing to the API
   */
  const safeData = xss(cloneDeep(data));

  return http.post('users', safeData);
};

Advantages

  • Keeps your components lean, removing responsibilities that are not merely presenting data to the user;
  • Separates your business logic from the framework code;
  • Facilitates testing by separating concerns in your smaller separate files;
  • Decreases the surface for bugs to appear because it eliminates code duplication.

Conclusion

This workflow proved scalable and easy to implement, understand, and teach.

Share this post