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.