I have created Axios wrapper following SOLID principles. Is there any feedback on improving it's structure in a more cleaner and better way?
axiosconfig.ts
export const axiosConfig = {
baseUrl : 'https://api.restful-api.dev/',
timeout: 120000,
headers: {
'Content-Type': 'application/json',
},
};
axioinstance.ts
import axios, { AxiosInstance } from 'axios';
import { axiosConfig } from './axiosConfig';
import { onRequest, onRequestError } from './requestInterceptors';
import { onResponse, onResponseError } from './responseInterceptors';
class AxiosInstanceCreator {
private static instance: AxiosInstance | null = null;
private constructor() {}
public static getInstance(): AxiosInstance {
if (!this.instance) {
const instance = axios.create(axiosConfig);
// Attach request interceptors
instance.interceptors.request.use(onRequest, onRequestError);
// Attach response interceptors
instance.interceptors.response.use(onResponse, onResponseError);
this.instance = instance;
// Set base URL globally
this.instance.defaults.baseURL = "https://api.restful-api.dev/";
}
return this.instance;
}
}
export default AxiosInstanceCreator.getInstance();
apiService.ts
class ApiService {
static async get<T>(url: string, params?: Record<string, any>): Promise<T> {
const response = await axiosInstance.get<T>(url, { params });
return response.data;
}
static async post<T>(url: string, data: any): Promise<T> {
const response = await axiosInstance.post<T>(url, data);
return response.data;
}
static async put<T>(url: string, data: any): Promise<T> {
const response = await axiosInstance.put<T>(url, data);
return response.data;
}
static async delete<T>(url: string): Promise<T> {
const response = await axiosInstance.delete<T>(url);
return response.data;
}
}
export default ApiService;
apiUrls.ts
export const ApiUrls = {
user: {
getAllUsers: '/objects',
getUserById: (id: number) => `/api/users/${id}`,
},
auth: {
login: '/api/auth/login',
register: '/api/auth/register',
},
product: {
listProducts: '/api/products',
getProductById: (id: number) => `/api/products/${id}`,
createProduct: '/api/products',
},
};
requestInterceptor.ts
import { AxiosRequestConfig, AxiosError, InternalAxiosRequestConfig } from 'axios';
// Function to handle successful request setup
export const onRequest = (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
const token = localStorage.getItem('authToken'); // Example: Attach a token
if(token) {
config.headers.Authorization = `Bearer ${token}`
}
// Do something before request is sent
return config;
};
// Function to handle request errors
export const onRequestError = (error: AxiosError): Promise<AxiosError> => {
console.error('[Request Error]:', error);
return Promise.reject(error); // Pass error to calling function
};
responseInterceptors.ts
import { AxiosError, AxiosResponse } from "axios";
export const onResponse = (response: AxiosResponse): AxiosResponse => {
// console.info(`[response] [${JSON.stringify(response)}]`);
return response;
};
export const onResponseError = (error: AxiosError): Promise<AxiosError> => {
// console.error(`[response error] [${JSON.stringify(error)}]`);
return Promise.reject(error);
};
-
\$\begingroup\$ Could you be more specific about what an Axios is? It's hard to know whether it's implemented effectively if we don't know the requirements. \$\endgroup\$Toby Speight– Toby Speight2024年12月19日 16:29:49 +00:00Commented Dec 19, 2024 at 16:29
-
3\$\begingroup\$ @TobySpeight It's a commonly used library in the JavaScript community to perform HTTP requests. It's obvious for JS developers, so maybe that is why he did not bother saying something about it. \$\endgroup\$Billal BEGUERADJ– Billal BEGUERADJ2024年12月19日 18:11:41 +00:00Commented Dec 19, 2024 at 18:11
-
\$\begingroup\$ So this is an implementation of that library? Fair enough. OTOH, if it's merely something using the library, then the title needs to be changed so that it summarises the purpose of the code rather than how it's implemented. \$\endgroup\$Toby Speight– Toby Speight2024年12月19日 18:15:20 +00:00Commented Dec 19, 2024 at 18:15
-
1\$\begingroup\$ Welcome to Code Review! This looks like a wrapper around the Axios library, and I have updated the wording accordingly, though if I misunderstood that then feel free to edit it. \$\endgroup\$Sᴀᴍ Onᴇᴌᴀ– Sᴀᴍ Onᴇᴌᴀ ♦2024年12月19日 19:32:36 +00:00Commented Dec 19, 2024 at 19:32
1 Answer 1
The code follows SOLID principles, but it reflects several practices commonly seen in Java or C#. While these aren't inherently wrong, there are a few deviations from typical JS/TS conventions, particularly regarding singleton patterns. Additionally, the excessive use of static methods isn't aligned with standard JS/TS practices.
Here’s my suggestion for an Axios wrapper, based on a pattern I use in production:
api-helper.ts
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios";
import { onRequest, onRequestError } from './requestInterceptors';
import { onResponse, onResponseError } from './responseInterceptors';
export class ApiHelper {
private axios: AxiosInstance;
constructor() {
const axiosConfig = {
baseUrl : 'https://api.restful-api.dev/',
timeout: 120000,
headers: {
'Content-Type': 'application/json',
},
};
this.axios = axios.create(axiosConfig);
this.axios.interceptors.request.use(onRequest, onRequestError);
this.axios.interceptors.response.use(onResponse, onResponseError);
}
async get(endpoint: string, props: AxiosRequestConfig = null) {
return this.axios.get(endpoint, props).then(res => res.data);
}
async post(endpoint: string, body: unknown, config: AxiosRequestConfig = null) {
return this.axios.post(endpoint, JSON.stringify(body), config).then(res => res.data);
}
async delete(endpoint: string, config: AxiosRequestConfig = null) {
return this.axios.delete(endpoint, config).then(res => res.data);
}
}
// TS way of creating a singleton
export const apiHelper = new ApiHelper();
While the apiUrls.ts structure works, for better scalability, you can create entity-specific helper classes. For example, you could create products-helper.ts, users-helper.ts, and auth-helper.ts. The term "helper" is what my team frequently uses, but you can replace it with terms like "handler," "service," or "manager" depending on your preference.
Example of users-helper.ts
import { apiHelper } from './api-helper';
class UsersHelper {
async getAllUsers(): Promise<User[]> {
const users = await apiHelper.get('/users');
return users as User[];
}
async getUserById(id: number): Promise<User> {
const user = await apiHelper.get(`/users/${id}`);
return user as User;
}
}
This approach keeps things modular and scalable while aligning with standard JS/TS practices. It also makes it easier to add new entity-specific logic or helpers in the future.
Explore related questions
See similar questions with these tags.