Making HTTP Requests With Axios in TypeScript

Written by rzhelieznov | Published 2022/10/18
Tech Story Tags: typescript | axios | tutorial | http-requests | javascript | javascript-development | typescript-tutorial | software-development

TLDRA couple of days ago in my current working project, we decided to rewrite our simple fetch service to a more powerful solution with the ability to cancel requests which were sent previously. We decided to use `axios` library and `TypeScript` as everybody has experience with it previously, and it provides a solution for request canceling based on Promises. It’s already working but will be extended in nearly future. All our services in the current project is a class-based, so we need to create a class `AxiosService` It will have 2 fields: instance which has a type 'AxiosInstance' and cancelTokenStatic.via the TL;DR App

A couple of days ago in my current working project, we decided to rewrite our simple fetch service to a more powerful solution with the ability to cancel requests which were sent previously. From the beginning, we decided to use axios library and TypeScript, as everybody has experience with it previously, and it provides a solution for request canceling based on Promises. It’s already working but will be extended in nearly future.

So, let’s start. Firstly, we need to create an Axios service. All our services in the current project is a class-based, so we need to create a class AxiosService. There are 2 base approaches of how we can use Axios: directly use axios object from import or create a new instance with axios.create. We will use the last one. Service will have 2 fields: instance which has a type AxiosInstance and cancelToken which has a type CancelTokenStatic:

class AxiosService {
    instance: AxiosInstance;
    cancelTokenStatic: CancelTokenStatic;
}

Then in the constructor, we create axiosInstanse:

constructor(url: string) {
    this.instance = axios.create({
        headers: {
            Accept: 'application/json',
            'Content-Type': 'application/json',
        },
        baseURL: url
    });
    this.cancelTokenStatic = axios.CancelToken;
}

axios.create function can take an object with options. In our case, we set field headers and told that our instance will work with json and provide a base URL address that will be used for every request. axios.CancelToken provide cancel requests logic, so we also save it

One more thing that we need is to create a public function that will update baseURL. We need it for some specific business cases:

setBaseURL(url: string): void {
    this.instance.defaults.baseURL = url;
}

Full version of AxiosService class:

import axios, { AxiosInstance, CancelTokenStatic } from 'axios';

class AxiosService {
    instance: AxiosInstance;
    cancelTokenStatic: CancelTokenStatic;

    constructor(url: string) {
        this.instance = axios.create({
            transformRequest: [
                (data, headers): string => {
                    headers['Authorization'] = `Bearer ${authService.getToken()}`;
                    return JSON.stringify(data);
                },
            ],
            headers: {
                Accept: 'application/json',
                'Content-Type': 'application/json',
            },
            baseURL: url,
        });
        this.cancelTokenStatic = axios.CancelToken;
    }

    setBaseURL(url: string): void {
        this.instance.defaults.baseURL = url;
    }
}

The next step is to create HttpService which will implement all other logic with requests. Why do we need to create a new service instead of just adding request logic to AxiosService? Because it will help us to use any other rest library instead of Axios if we want to do this in the future.

HttpService will have 3 fields:

class HttpService {
    private axiosInstance: AxiosInstance;
    private sourceMap: { [key: string]: CancelTokenSource[] } = {};
    private cancelToken: CancelTokenStatic;
}

axiosInstance and cancelToken we will set in constructor, sourceMap will be our store where we will keep cancel tokens for requests

constructor(instance: AxiosInstance, cancelToken: CancelTokenStatic) {
    this.axiosInstance = instance;
    this.cancelToken = cancelToken;
}

Now let’s implement the logic for canceling requests.

private createCancelToken = (cancelKey: string): CancelToken => {
    const source = this.cancelToken.source();

    if (this.sourceMap[cancelKey]) {
        this.sourceMap[cancelKey].push(source);
    } else {
        this.sourceMap[cancelKey] = [source];
    }

    return source.token;
};

Private function createCancelToken firstly create a new cancel token source. Then we check if our sourceMap already have a field with key cancelKey, then we just add new cancelSource to array or we create a new field in sourceMap and the source is the first element in the array.

The next step is to create cancelPreviousRequests method.

cancelPreviousRequests = (cancelKey: string): void => {
    const requests = this.sourceMap[cancelKey] || [];
    
    requests.forEach((item) => {
        item.cancel('cancel');
    });

    this.sourceMap[cancelKey] = [];
};

We take a cancelKey from the argument and find such field in sourceMap. Then we iterate through every CancelTokenSource in it and call the cancel function. And then delete the target field in sourceMap via saving an empty array to it.

Now we can start with requests logic. We will use the general method sendRequest for all requests. This method will take some options. Let’s define interface for them:

interface RequestOptions {
    url: string; // url for request
    method?: Method; // HTTP method
    params?: Dictionary<any> | string; // request params
    cancelKey?: string; // key for sourceMap if request can be canceled
  responseType?: ResponseType; // type for response
}

And the sendRequest method which will return Promise:

sendRequest<T = unknown>({
    url,
    method = 'GET',
    params,
    cancelKey,
    responseType,
  }: RequestOptions): Promise<AxiosResponse<T> | void> {
    let cancelToken;

    if (cancelKey) {
        cancelToken = this.createCancelToken(cancelKey);
    }

    switch (method) {
        case 'POST':
            return this.postRequest({ url, data: params, cancelToken });

        default:
            return this.getRequest({
                url,
                cancelToken,
                responseType,
            });
    }
}

We use generic for returned type for response and method we set to GET by default. Firstly, we check if we have a cancelKey, then we need to create cancelToken and push it to sourceMap. Then we use switch construction to iterate through the request’s methods and call corresponding functions.

The request function also will take some params, so we need to define an interface for them:

type RequestParams = {
    url: string;
    cancelToken?: CancelToken;
    data?: Dictionary<any> | string;
};

We have 2 base HTTP methods for POST and GET requests:

private async postRequest({
    url,
    data,
    cancelToken,
}: RequestParams): Promise<AxiosResponse | void> {
    try {
        return await this.axiosInstance.post(url, data, { cancelToken });
    } catch (error) {
      return Promise.reject(error);
    }
}

private async getRequest({
    url,
    cancelToken,
}: RequestParams): Promise<AxiosResponse | void> {
    try {
        return await this.axiosInstance.get(url, { cancelToken });
    } catch (error) {
        return Promise.reject(error);
    }
}

Nothing special here. We just call the corresponding request from axiosInstance with our params

Full code for HttpService:

import {
    AxiosResponse,
    Method,
    CancelTokenSource,
    CancelToken,
    AxiosInstance,
    CancelTokenStatic,
} from 'axios';
import { axiosService } from './AxiosService';

interface RequestOptions {
    url: string;
    method?: Method;
    params?: Dictionary<any> | string;
    cancelKey?: string;
}

interface RequestParams {
    url: string;
    cancelToken?: CancelToken;
    data?: Dictionary<any> | string;
}

class HttpService {
    private axiosInstance: AxiosInstance;
    private sourceMap: { [key: string]: CancelTokenSource[] } = {};
    private cancelToken: CancelTokenStatic;

    constructor(instance: AxiosInstance, cancelToken: CancelTokenStatic) {
        this.axiosInstance = instance;
        this.cancelToken = cancelToken;
    }

    private createCancelToken = (cancelKey: string): CancelToken => {
        const source = this.cancelToken.source();
        if (this.sourceMap[cancelKey]) {
            this.sourceMap[cancelKey].push(source);
        } else {
            this.sourceMap[cancelKey] = [source];
        }
        return source.token;
    };

    cancelPreviousRequests = (cancelKey: string): void => {
        const requests = this.sourceMap[cancelKey] || [];
        requests.forEach((item) => {
            item.cancel('cancel');
        });
        this.sourceMap[cancelKey] = [];
    };

    private async postRequest({
        url,
        data,
        cancelToken,
    }: RequestParams): Promise<AxiosResponse | void> {
        try {
            return await this.axiosInstance.post(url, data, { cancelToken });
        } catch (error) {
            return Promise.reject(error);
        }
    }

    private async getRequest({ url, cancelToken }: RequestParams): Promise<AxiosResponse | void> {
        try {
            return await this.axiosInstance.get(url, { cancelToken });
        } catch (error) {
            return Promise.reject(error);
        }
    }

    sendRequest<T = unknown>({
        url,
        method = 'GET',
        params,
        cancelKey,
    }: RequestOptions): Promise<AxiosResponse<T> | void> {
        let cancelToken;

        if (cancelKey) {
            cancelToken = this.createCancelToken(cancelKey);
        }

        switch (method) {
            case 'POST':
                return this.postRequest({ url, data: params, cancelToken });

            default:
                return this.getRequest({
                    url,
                    cancelToken,
                });
      }
}

Example of usage:

httpService.cancelPreviousRequests('testRequest');
httpService
    .sendRequest<string[]>({
        url: '/api/testurl',
        cancelKey: 'testRequest',
    })
    .then((response) => {
        if (response?.data) {
            console.log(response.data)
        }
    })
    .catch((error) => {
        console.warn(error);
    });


Written by rzhelieznov | Javascript and React fan. Also love reading, traveling, pc gaming :)
Published by HackerNoon on 2022/10/18