@seriouslag/httpclient
Version:
Typed wrapper HttpClient for axios
213 lines (196 loc) • 6.04 kB
text/typescript
import {
IHttpClientAdaptor,
Method,
RequestConfig,
ResponseType,
HttpResponse,
} from './Adaptors';
import { FetchClientAdaptor } from './FetchClientAdaptor';
import {
HttpRequestStrategy,
DefaultHttpRequestStrategy,
} from './HttpRequestStrategies';
import { Logger } from './Logger';
import { AbortError } from './errors/AbortError';
import { ABORT_MESSAGE, ERROR_URL } from './strings';
/** Config used for setting up http calls */
export interface ApiConfig {
/** If specified, a new axios instance is used instead of the one instantiated in the HttpClient's constructor */
noGlobal?: boolean;
/** The headers that will be used in the HTTP call. Global headers will be added to these.
* // TODO: Test when noGlobal is true if global headers are added to the request
*/
headers?: Record<string, string>;
/** The body of the request that will be sent */
data?: any;
/** The type of response that will be expected */
responseType?: ResponseType;
/** The query parameters that will be sent with the HTTP call */
params?: any;
/** The encoding of the response */
responseEncoding?: string;
/** The strategy to use for this request, if not provided then the request that was provided with the HttpClient will be used */
httpRequestStrategy?: HttpRequestStrategy;
}
/**
* HttpClient configuration options
*/
export interface HttpClientOptions {
/** The strategy that will be used to handle http requests */
httpRequestStrategy?: HttpRequestStrategy;
/** The logger the HttpClient will use */
logger?: Logger;
baseUrl?: string;
}
/** Typed wrapper around axios that standardizes making HTTP calls and handling responses */
export class HttpClient {
private logger: Logger | undefined;
private httpRequestStrategy: HttpRequestStrategy;
private baseUrl: string;
constructor(
private httpClientAdaptor: IHttpClientAdaptor = new FetchClientAdaptor(),
options: HttpClientOptions = {},
) {
const { httpRequestStrategy, logger, baseUrl = '' } = options;
this.httpRequestStrategy =
httpRequestStrategy ?? new DefaultHttpRequestStrategy();
this.logger = logger;
this.baseUrl = baseUrl;
}
/**
* Sets the logger for the instance
* @param {Logger|undefined} logger
*/
public setLogger(logger: Logger | undefined) {
this.logger = logger;
}
/** HTTP GET request */
public get<T = unknown>(
url: string,
config: ApiConfig = {},
cancelToken?: AbortController,
): Promise<T> {
const method: Method = 'get';
return this.dataRequest<T>(url, method, config, cancelToken);
}
/** HTTP POST request */
public post<T = unknown>(
url: string,
config: ApiConfig = {},
cancelToken?: AbortController,
): Promise<T> {
const method: Method = 'post';
return this.dataRequest<T>(url, method, config, cancelToken);
}
/** HTTP PUT request */
public put<T = unknown>(
url: string,
config: ApiConfig = {},
cancelToken?: AbortController,
): Promise<T> {
const method: Method = 'put';
return this.dataRequest<T>(url, method, config, cancelToken);
}
/** HTTP DELETE request */
public delete<T = unknown>(
url: string,
config: ApiConfig = {},
cancelToken?: AbortController,
): Promise<T> {
const method: Method = 'delete';
return this.dataRequest<T>(url, method, config, cancelToken);
}
/** HTTP PATCH request */
public patch<T = unknown>(
url: string,
config: ApiConfig = {},
cancelToken?: AbortController,
): Promise<T> {
const method: Method = 'patch';
return this.dataRequest<T>(url, method, config, cancelToken);
}
/**
* HTTP request that returns the body of the HTTP response
*
* If a cancel token is passed in it will be aborted on request error.
*
* @returns {Promise<T>} body of the HTTP response
*/
public async dataRequest<T = unknown>(
url: string,
method: Method,
config: ApiConfig = {},
cancelToken?: AbortController,
): Promise<T> {
const response = await this.request<T>(url, method, config, cancelToken);
return response.data;
}
/**
* HTTP request
*
* If a cancel token is passed in it will be aborted on request error.
*
* @returns {Promise<HttpResponse<T>>} HttpResponse
*/
public async request<T = unknown>(
url: string,
method: Method,
config: ApiConfig = {},
cancelToken?: AbortController,
): Promise<HttpResponse<T>> {
if (cancelToken?.signal.aborted) {
throw new AbortError(ABORT_MESSAGE);
}
try {
return await this.doRequest<T>(url, method, config, cancelToken);
} catch (e) {
let message = `The ${method} request to ${url} failed`;
if (e && typeof e === 'object' && 'status' in e) {
message += ` with status ${e.status}`;
}
cancelToken?.abort(message);
throw e;
}
}
private async doRequest<T = unknown>(
url: string,
method: Method,
config: ApiConfig = {},
cancelToken?: AbortController,
): Promise<HttpResponse<T>> {
if (typeof url !== 'string') throw new Error(ERROR_URL);
const {
headers,
data,
params,
responseEncoding,
responseType,
httpRequestStrategy,
noGlobal,
} = config;
const strategyToUse = httpRequestStrategy ?? this.httpRequestStrategy;
const requestConfig: RequestConfig = {
url: this.baseUrl + url,
method,
headers,
data,
params,
responseEncoding,
responseType,
cancelToken,
noGlobal,
};
try {
const request = this.httpClientAdaptor.buildRequest<T>(requestConfig);
this.logger?.debug(`HTTP - method: ${method}; url: ${url}`);
const response = await strategyToUse.request<T>(request);
this.logger?.debug(
`HTTP ${response.status} - method: ${method}; url: ${url}`,
);
return response;
} catch (e) {
this.logger?.error(`HTTP error - method: ${method}; url: ${url}`, e);
throw e;
}
}
}