Sign in
Log inSign up

Handling API calls in Vue

Sultan Iman's photo
Sultan Iman
·May 14, 2018

As a full time Vue developer at home I've been exploring different ways of making API calls and integrating them into store. My approach I settled with for some time already works fine for my own private project.

So my approach is to wrap Axios calls and provide similar methods and delegate additional business logic related to preparing and sending requests to API. Initially the code below used browser fetch but it was somewhat problematic to cancel requests and track progress so Axios fulfilled those requirements.

/*
 * Wraps axios and provides
 * more convenient post method
 * calls with payload data
 */
export function post(uri, data) {
  return axios.post(uri, data, {
    headers: getHeaders(),
    withCredentials: true
  })
}

/*
 * Wraps axios and provides
 * more convenient put method
 * calls with data
 */
export function put(uri, data) {
  return axios.put(uri, data, {
    headers: getHeaders(),
    withCredentials: true
  })
}

/*
 * Wraps axios and provides
 * more convenient delete method
 */
export function remove(uri) {
  return axios.delete(uri, {
    headers: getHeaders(),
    withCredentials: true
  })
}

/*
 * Wraps axios and provides
 * more convenient get method
 * calls with data.
 */
export function get(uri, data = {}) {
  if (Object.keys(data).length > 0) {
    uri = `${uri}?${qs(data)}`
  }

  return axios.get(uri, {
    headers: getHeaders(),
    withCredentials: true
  })
}

export function upload(uri, data) {
  return fetch(uri, {
    headers: getHeaders(true),
    cors: true,
    method: 'POST',
    body: data
  })
}

As you can see everything is pretty straight forward, except file upload for which I decided to stick to browser fetch. Also by hiding all intermediate request configuration we can focus on what we expect from API call and what data is communicated to API. Next part is helper functions like error handling, preparing headers, setting JWT token and providing simple functions to escape and compose query parameters.

/*
 * Returns default headers list
 * which will be used with every request.
 */
function getHeaders(multipart = false) {
  let defaultHeaders = BASE_HEADERS

  if (multipart) {
    defaultHeaders = {}
  }

  if (localStorage.token) {
    defaultHeaders = {
      'Authorization': `JWT ${localStorage.token}`,
      ...defaultHeaders
    }
  }

  return defaultHeaders
}

/*
 * Wraps error responses from the API
 * and returns `Promise.reject` with error
 */
export function checkResponse(response) {
  if (validStatuses.includes(response.status)) {
    return response
  }

  // If not authorized then reset token
  // and redirect to login page
  if (response.status === 401) {
    localStorage.removeItem('token')
    router.push('login')

    return Promise.reject(new Error('USER_ANONYMOUS'))
  }

  let err = new Error(response.statusText)
  err.response = response

  return Promise.reject(err)
}

// Just a convenient shorthand
export const esc = encodeURIComponent

// Returns formatted query string from object
export function qs(params) {
  return (
    Object
    .keys(params)
    .map(k => esc(k) + '=' + esc(params[k]))
    .join('&')
  )
}

Also you might want to ask about default global request configuration and for it there is simple configuration.

axios.defaults.baseURL = 'api.awesome-project.com'

Server errors handled by defining an interceptors and passing an error handler. We can always add advanced integrations with Axios but native fetch looks promising because big players are already investing efforts to make it widely available (it is available already but you have to cope with limitations).

Once Vuex gets integrated at some point you will have necessity to make API calls and putting all API calls inside Vuex actions is not bad except the moment when it starts growing it becomes quite hard to follow on what is going on. Lets say we have Vuex actions responsible for handing user's session so we need to obtain

  • JWT token,
  • May be get CSRF token,
  • May be we need login method.

Taking these into account lets look at the the following sample creates "mental" overhead and if there are more than a dozen of actions then the chances are you get lost or get slowed down are pretty high

import axios from 'axios'

export function getCSRFToken() {
  return axios.get('/api/security/csrf/token')
        .then(response => {
          // Save token
          // May be call next may be it is enough
        })
        .catch(error => {
          if (error.status === 401) {
            // Bla bla
          }

          if (error.status === 400) {
            // Show form errors or whatever necessary to notify user
          }

          // And so on there might be many error and reasons to handle them
        })
}

export function verifyToken(token) {
  token = token || localStorage.token

  if (token) {
    return axios.post('/api/auth/token/verify', {token})
          .then(response => {...})
          .catch(error => {
            if (error.status === 401) {
              // Bla bla
            }

            if (error.status === 400) {
              // Show form errors or whatever necessary to notify user
            }

            // And so on there might be many error and reasons to handle them
          })
  }

  return Promise.reject(new Error('ERR_NO_TOKEN'))
}

export function loginUser(credentials) {
  return axios.post('/api/auth/token', credentials)
        .then(response => {doSomeUsefullStuff})
        .catch(error => {
          if (error.status === 401) {
            // Bla bla
          }

          if (error.status === 400) {
            // Show form errors or whatever necessary to notify user
          }

          // And so on there might be many error and reasons to handle them
        })
}

Now lets compare those to refined actions with clear domain boundaries

import { checkResponse, get, post } from '@/helpers/http'

export function getCSRFToken() {
  return get('/api/security/csrf/token')
        .then(checkResponse)
}

export function verifyToken(token) {
  token = token || localStorage.token

  if (token) {
    return post('/api/auth/token/verify', {token})
          .then(checkResponse)
  }

  return Promise.reject(new Error('ERR_NO_TOKEN'))
}

export function loginUser(credentials) {
  return post('/api/auth/token', credentials)
        .then(checkResponse)
}

It is pretty easy to follow and see what it going on and "mental" overhead compared to previous example is ok for me.

Ok these are just helpers whats next where are the real actions?

So my approach to to have additional domain layer which knows how to talk with our API thus Vuex actions only knows how to call them and pass required parameters.

import { loginUser, getCSRFToken, verifyToken } from '@/api/security'

export default {
  /*
   * Check if token set
   * If token set
   * Then verify token
   * If token is valid
   * Then parse it and save it
   * Also show error messages
   * in case if session has expired.
   */
  [CHECK_SESSION] ({commit, dispatch, getters}, router) {
    const token = localStorage.getItem('token')

    verifyToken(token)
    .then(transitionToDashboard)
  },

  /*
   * Login user with email and password
   * and set token and after parse it
   * and set parsed user info.
   */
  [LOGIN] ({dispatch}, loginInfo) {
    loginUser(loginInfo)
    .then(saveJWTToken)
  },

  [GET_CSRF_TOKEN] ({commit}) {
    getCSRFToken()
    .then(saveCSRFToken)
  }
}

As you can see store actions just know what to call and how to call the rest of the logic like processing errors and keeping additional business logic is unnecessary.

Sidenote: My style of defining actions looks pretty similar to Redux, thats because past experience as a React developer.

I will appreciate any feedback, review and relevant critiques.

What is your experience?

Thanks.