Axios, Typescript and Vue 3

In this post, we will aim to create a clean architecture for accessing a rest endpoint, using Axios with Typescript and Vue 3. The key focus here would be to how structure your typescript classes to increase code reuse and isolating the actual axios calls.

The approach I follow now is based on building an axios wrapper, namely HttpClient, which would be used by a set of ApiService to access the endpoints. Let us first define the HttpClient.

import { IResponseBase } from "@/types/ApiRequestResponseTypes";
import axios, {AxiosInstance, AxiosRequestConfig, AxiosError, AxiosResponse} from "axios";

class HttpClient{

    private axiosInstance : AxiosInstance;
    
    constructor(){
        const headers = {
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Headers": "*", // this will allow all CORS requests
            "Access-Control-Allow-Methods": "OPTIONS,POST,GET", // this states the allowed methods
            "Content-Type": "application/json", // this shows the expected content type
          };
        this.axiosInstance = axios.create({baseURL: "http://localhost:8001/", headers:headers});
    }

    public async invoke<T extends IResponseBase,R = AxiosResponse<T>>(config:AxiosRequestConfig):Promise<T> {

        try{
            const response =  await this.axiosInstance.request<T>(config); 
            return response.data;
        }catch(error : AxiosError | any){

            if(axios.isAxiosError(error)){
                return <T>{
                    status : error.response?.status,
                    hasError : true,
                    errors : error.response?.data.errors
                } 
            }
            else{
                console.log("Some other error ?? " + error);
            }
        } 

        return <T>{};

        
    }
}

export default HttpClient;

As you can observe, the HttpClient uses the Axios library for accesing the endpoints. In the constructor, we are initializing the axiosInstance with the baseUrl and header.

The class exposes a single method invoke which accepts a single parameter data. Additionally, it accepts a generic parameter T, which is the expected return type. The T generic parameter is constraint by IResponseBase – an interface which we will shortly define.

The other interesting part in the function above is how errors are handled. We use the same IResponseBase derievatives to return the error information. Let us now define IResponseBase and one of its derieved type.

export interface IResponseBase {
    hasError: boolean,
    status?: number,
    errors?: Array<string>
}

The IResponseBase type has 3 properties, to describe the error. The data part is handled by the specific derivatives. For example,

export interface IRegisterUserResponse extends IResponseBase {
    data: {
        userName: string
    }
}

We will now define a base for our ApiServices.

import { IResponseBase } from "@/types/ApiRequestResponseTypes";
import { AxiosRequestConfig, AxiosResponse } from "axios";
import HttpClient from "./HttpClient";


export abstract class ApiServiceBase
{
    private httpClient : HttpClient;

    constructor(){
        this.httpClient = new HttpClient();
    }

    protected async invoke<T extends IResponseBase,R = AxiosResponse<T>>(config:AxiosRequestConfig):Promise<T> {
        return this.httpClient.invoke(config);
    }
}

The abstract class ApiServiceBase uses the HttpClient to access the Web endpoints. Let us now define our concrete (sample) Api Service class.

import { IRegisterUserRequest, IRegisterUserResponse} from "../types/ApiRequestResponseTypes";
import{AxiosResponse} from "axios";
import { ApiServiceBase } from "./ApiServiceBase";

class UserApiService extends ApiServiceBase {

    public async registerUser(user:IRegisterUserRequest):Promise<IRegisterUserResponse>{
        
        return await this.invoke<IRegisterUserResponse,AxiosResponse<IRegisterUserResponse>>({method:'post', url:"/user/createuser", data : user});
    }
}

export const userApiService = new UserApiService();

As seen the UserApiService provides the implementation for registerUser. As you can see, with the infrastructure we have laid, it becomes easier to build new Api services. There is definetly ways to improve the implementation, and I would really like to do another post soon with an updated version of the implemention.

Leave a comment