Skip to content

Instantly share code, notes, and snippets.

@estevan-ulian
Last active April 7, 2025 16:47
Show Gist options
  • Save estevan-ulian/2717c247c4387caf7eed180237b6efec to your computer and use it in GitHub Desktop.
Save estevan-ulian/2717c247c4387caf7eed180237b6efec to your computer and use it in GitHub Desktop.
HttpClient instance with Axios
interface IHttpError {
name: string;
message: string;
status: number;
details?: unknown;
}
const HttpMethod = {
GET: "GET",
POST: "POST",
PUT: "PUT",
DELETE: "DELETE",
PATCH: "PATCH",
} as const;
type IHttpMethod = (typeof HttpMethod)[keyof typeof HttpMethod];
interface IHttpRequest<B> {
url?: string;
method?: IHttpMethod;
headers?: Record<string, string>;
body?: B;
}
interface IHttpResponse<R> {
data: R | null;
error: IHttpError | null;
}
interface IHttpClient {
request<B, R>(request: IHttpRequest<B>): Promise<IHttpResponse<R>>;
}
class HttpClient implements IHttpClient {
private static instance: HttpClient;
private constructor(
private httpHandler: AxiosInstance,
private baseUrl: string,
private isDebugMode: boolean = false
) {
this.validateParams();
this.setupInterceptors();
}
static create(params: { httpHandler: AxiosInstance; baseUrl: string }) {
if (!HttpClient.instance) {
HttpClient.instance = new HttpClient(params.httpHandler, params.baseUrl);
} else {
if (HttpClient.instance.httpHandler !== params.httpHandler) {
throw new Error("httpHandler cannot be changed after initialization");
}
HttpClient.instance.updateBaseUrl(params.baseUrl);
}
return HttpClient.instance;
}
updateBaseUrl(newBaseUrl: string) {
this.baseUrl = newBaseUrl;
}
private validateParams() {
if (!this.httpHandler) {
throw new Error("httpHandler is required");
}
if (!this.baseUrl) {
throw new Error("baseUrl is required");
}
}
private setupInterceptors() {
this.httpHandler.interceptors.request.use((config) => {
if (this.isDebugMode) {
const logStyle = "color: #2196F3; font-weight: bold;";
const logMessage = `%cRequest: ${config.method?.toUpperCase()} ${
config.url
}`;
const logMessageHeaders = `%cRequestHeaders: ${JSON.stringify(
config.headers,
null,
2
)}`;
const logMessageData = `%cRequestData: ${JSON.stringify(
config.data,
null,
2
)}`;
console.log(logMessage, logStyle);
console.log(logMessageHeaders, logStyle);
console.log(logMessageData, logStyle);
}
return config;
});
this.httpHandler.interceptors.response.use(
(response) => {
if (this.isDebugMode) {
const { data } = response.data;
const logStyle = "color: #4CAF50; font-weight: bold;";
const logMessageStatus = `%cResponseStatus: ${response.status} ${response.statusText}`;
const logMessageData = `%cResponseData: ${JSON.stringify(
data,
null,
2
)}`;
console.log(logMessageStatus, logStyle);
console.log(logMessageData, logStyle);
}
return response;
},
(error) => {
console.error(error);
return Promise.reject(error);
}
);
}
async request<B, R>(request: IHttpRequest<B>): Promise<IHttpResponse<R>> {
const { url = "", body = {}, headers = {}, method = "GET" } = request;
try {
const response = await this.httpHandler.request<R>({
baseURL: this.baseUrl,
url,
method,
data: body,
headers,
});
const data = {
data: response.data,
error: null,
};
return data;
} catch (error: unknown) {
if (isAxiosError(error)) {
return this.handleAxiosError(error);
} else {
return this.handleUnexpectedError(error);
}
}
}
private handleAxiosError(error: AxiosError) {
return {
data: null,
error: {
name: error.name,
message: error.message,
status: error.response?.status || 400,
details: null,
},
};
}
private handleUnexpectedError(error: unknown) {
return {
data: null,
error: {
name: "UnexpectedError",
message: "An unexpected error occurred",
status: 500,
details: JSON.stringify(error),
},
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment