There were times I needed a backend framework to support making api request. Axios offers a powerful and flexible HTTP client that simplifies the process of making HTTP requests and handling responses in JavaScript/TypeScript applications. Its ease of use, promise-based nature, and extensive features make it a great candidate. To do this I created a client that looked like the following:

import axios, { AxiosResponse } from 'axios';
import { expect } from 'chai';
import https from 'https';
import Logger from '../logger/winston';
import { v4 as uuid } from 'uuid';
interface RequestInterface {
  url: string;
  method: 'post' | 'put' | 'patch' | 'delete' | 'get';
  headers?: Object;
  body?: Object;
  params?: Object;
  expectedFailureResponse?: {
    statusCode: number;
    message?: string;
  };
}

const client = axios.create({
  httpsAgent: new https.Agent({ keepAlive: true, maxSockets: 50 }),
  timeout: 25000,
  proxy: false,
});

const appendToQueryString = (
  url: string,
  param: string,
  value: string
): string => {
  const regex = new RegExp('([?&])' + param + '=.*?(&|$)', 'i');
  const separator = url.indexOf('?') !== -1 ? '&' : '?';
  if (url.match(regex)) {
    return url.replace(regex, '$1' + param + '=' + value + '$2');
  } else {
    return url + separator + param + '=' + value;
  }
};

export const request = async (
  requests: RequestInterface[]
): Promise<AxiosResponse[]> => {
  const promises = requests.map(async (request) => {
    const { expectedFailureResponse } = request;
    let retries = 0;
    let failureOccurred = false;
    let success = false;
    const maxRetries = 2;
    let url = appendToQueryString(request.url, 'automationId', uuid());
    while (retries < maxRetries && !success) {
      try {
        switch (request.method) {
          case 'post':
            expect(request.body, 'POST missing request body').to.be.ok;
            const postRequest = await client.request({
              method: 'post',
              url: url,
              headers: request.headers ? request.headers : {},
              data: request.body,
            });
            success = true;
            return postRequest;
          case 'put':
            expect(request.body, 'PUT missing request body').to.be.ok;
            const putRequest = await client.request({
              method: 'put',
              url: url,
              headers: request.headers ? request.headers : {},
              data: request.body,
              params: request.params ? request.params : '',
            });
            success = true;
            return putRequest;
          case 'patch':
            expect(request.body, 'PATCH missing request body').to.be.ok;
            const patchRequest = await client.request({
              method: 'patch',
              url: url,
              headers: request.headers ? request.headers : {},
              data: request.body,
            });
            success = true;
            return patchRequest;
          case 'delete':
            const deleteRequest = await client.request({
              method: 'delete',
              url: url,
              headers: request.headers ? request.headers : {},
              data: request.body ? request.body : '',
            });
            success = true;
            return deleteRequest;
          case 'get':
            expect(request.body, 'GET request should not have request body').to
              .be.undefined;
            const getRequest = await client.request({
              method: 'get',
              url: url,
              headers: request.headers ? request.headers : {},
              params: request.params ? request.params : '',
            });
            success = true;
            return getRequest;
          default:
            return;
        }
      } catch (error) {
        if (!expectedFailureResponse) {
          Logger.error(`URL: ${url}`);
          Logger.error(`METHOD: ${request.method}`);
          if (request.body) {
            Logger.error(`Request Body:`, {
              message: JSON.stringify(request.body),
            });
          }
          if (request.params) {
            Logger.error(`Params: ${JSON.stringify(request.params)}`);
          }
          Logger.error(error.toJSON());
          if (error.response) {
            const { data, status, headers } = error.response;
            /*
             * The request was made and the server responded with a
             * status code that falls out of the range of 2xx
             */
            if (status) Logger.error(`Status ${status}`);
            if (headers)
              Logger.error('Headers', { message: JSON.stringify(headers) });
            if (data) {
              Logger.error('Error', { message: JSON.stringify(data) });
              if (data.message)
                Logger.error(`Response error message`, {
                  message: JSON.stringify(data.message),
                });
            }
          } else if (error.request) {
            /*
             * The request was made but no response was received, `error.request`
             * is an instance of XMLHttpRequest in the browser and an instance
             * of http.ClientRequest in Node.js
             */
            Logger.error('Request error: ', error.request);
            if (error.message) {
              Logger.error('Request error message: ', {
                message: JSON.stringify(error.message),
              });
            }
          } else {
            // Something happened in setting up the request and triggered an Error
            Logger.error('Error', { message: JSON.stringify(error.message) });
          }
          retries++;
        } else {
          failureOccurred = true;
          success = true;
          if (expectedFailureResponse) {
            expect(error.response.status).to.eq(
              expectedFailureResponse.statusCode
            );
            if (expectedFailureResponse.message) {
              expect(error.response.data.message).to.eq(
                expectedFailureResponse.message
              );
            }
          }
        }
      } finally {
        if (expectedFailureResponse) {
          Logger.warn('Expected Failed Request');
          Logger.warn(`URL: ${url}`);
          Logger.warn(`METHOD: ${request.method}`);
          if (request.body) {
            Logger.warn(`Request Body:`, {
              message: JSON.stringify(request.body),
            });
          }
          if (request.params) {
            Logger.warn(`Params: ${JSON.stringify(request.params)}`);
          }
          Logger.warn(`Expected Failure:`, {
            message: JSON.stringify(expectedFailureResponse),
          });
          expect(
            failureOccurred,
            'Expected api to fail but request was successful'
          ).to.eq(true);
        }
      }
    }
  });
  const data = await Promise.all(promises);
  return data;
};

Things to note:

  • Retry logic exists
    • Should probably have made the retry count an env variable
  • Expected Errors are handled
  • Ability to pass in an array of multiple request at a time
  • Automation ID’s are added for easier debugging when looking at automation specific request.