Skip to main content

Axios Configuration

HTTP client configuration and API request utilities for the WorkPayCore frontend application with multi-instance setup, authentication, and EOR portal integration.

Overview

The axios configuration provides three specialized HTTP client instances with built-in authentication, EOR (Employer of Record) portal support, and automatic token refresh functionality.

HTTP Client Instances

httpV2 (Primary Instance)

The main HTTP client instance with full authentication and EOR portal integration.

Configuration:

import { httpV2 } from '@/api/axios';

// Base configuration
const httpV2 = axios.create({
baseURL: BASE_API_V2_ENDPOINT,
params: { ...defaultAuthAdminParams },
});

Default Parameters:

  • auth_employee_id: Employee ID for employee view context
  • authorize_for: Authorization scope
  • token: User authentication token
  • auth_company_id: Company ID for multi-tenant operations

Use Cases:

  • Primary API operations
  • Authenticated requests
  • EOR portal operations
  • Employee management
  • Company-specific data access

httpV3 (Bearer Token Instance)

HTTP client with Bearer token authentication for modern API endpoints.

Configuration:

import { httpV3 } from '@/api/axios';

// Bearer token configuration
const httpV3 = axios.create({
baseURL: BASE_API_V3_ENDPOINT,
headers: {
Authorization: `Bearer ${USER_TOKEN}`,
Accept: 'Application/json',
},
params: {
...defaultAuthAdminParams,
employee_id: isEmployeeView() ? currentUser()?.employee_id : null,
},
});

Use Cases:

  • Modern REST API endpoints
  • Bearer token authentication
  • Employee-specific operations
  • New API implementations

http (Basic Instance)

Simple HTTP client for unauthenticated or basic requests.

Configuration:

import { http } from '@/api/axios';

// Basic configuration
const http = axios.create({
baseURL: BASE_API_V1_ENDPOINT,
});

Use Cases:

  • Public API endpoints
  • Authentication requests
  • Health checks
  • Static data retrieval

Authentication System

Default Authentication Parameters

export const defaultAuthAdminParams = {
auth_employee_id: isEmployeeView()
? Number(CURRENT_AUTH_USER_ROLE?.employee_id) >= 1 &&
!CURRENT_AUTH_USER_ROLE?.is_onboarding
? CURRENT_AUTH_USER_ROLE?.employee_id
: null
: null,
authorize_for: null,
token: USER_TOKEN,
auth_company_id: AUTH_COMPANY_ID,
};

Token Management

// Automatic token refresh from response headers
httpV2.interceptors.response.use(
async response => {
const authHeader =
response?.headers?.authorization || response?.headers?.Authorization;

if (!isNull(authHeader)) {
await store.dispatch(
signUpRequestSuccess({
auth_token: authHeader,
}),
);
}

return response;
},
// Error handling...
);

Authentication Error Handling

// 401 Unauthorized handling
httpV2.interceptors.response.use(
response => response,
async error => {
if (error?.response?.status === 401) {
if (!isNull(USER_TOKEN)) {
useAuthStore.getState().setOpen(true);
useAuthStore.getState().setLastLoginEmail(currentUser()?.email);
}
}
return Promise.reject(error);
},
);

EOR Portal Integration

EOR Portal Parameters

export const getDefaultEorPortalParams = () => {
const company = getEorFilter('eor_company_single_select');
const payrollEorCompanyID = company?.value || undefined;

if (isEorPortalView()) {
return {
is_eor: 1,
auth_company_id: Number.isNaN(Number(payrollEorCompanyID))
? AUTH_COMPANY_ID
: payrollEorCompanyID,
};
}

return {};
};

EOR Request Interceptor

httpV2.interceptors.request.use(
config => {
if (!isEorPortalView()) {
return config; // Exit early if not in EOR portal context
}

const isExcludedUrl = EXCLUDED_EOR_ENDPOINTS?.some(
url => config.url?.includes(url),
);

if (isExcludedUrl) {
return config; // Exit early if URL is excluded
}

const eorParams = getDefaultEorPortalParams();
config.params = { ...config.params, ...eorParams };

// Handle different HTTP methods
const methodsRequiringBodyUpdate = new Set([
'put',
'post',
'delete',
'patch',
]);

if (methodsRequiringBodyUpdate.has(config.method?.toLowerCase())) {
if (config.data instanceof FormData) {
Object.entries(eorParams).forEach(([key, value]) => {
config.data.set(key, value);
});
} else if (config.data && typeof config.data === 'object') {
config.data = { ...config.data, ...eorParams };
}
}

return config;
},
error => Promise.reject(error),
);

Excluded EOR Endpoints

// Endpoints that bypass EOR parameter injection
const EXCLUDED_EOR_ENDPOINTS = [
'/auth',
'/public',
'/health',
// ... other excluded endpoints
];

Generic API Request Function

apiRequest Utility

const apiRequest = async <T = any,>({
method = 'GET',
url,
id,
payload,
query,
useLegacyAPI = false,
responseType,
}: {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
url: string;
id?: number | string;
payload?: any;
query?: Record<string, any>;
useLegacyAPI?: boolean;
responseType?: string;
}): Promise&lt;T&gt; => {
const requestUrl = id ? `${url}/${id}` : url;

const config: any = {
method,
url: requestUrl,
params: query,
data: payload,
useLegacyAPI,
responseType,
};

return httpV2.request(config);
};

Usage Examples

// GET request
const users = await apiRequest<User[]>({
url: '/users',
query: { page: 1, limit: 10 },
});

// POST request
const newUser = await apiRequest&lt;User&gt;({
method: 'POST',
url: '/users',
payload: { name: 'John Doe', email: 'john@example.com' },
});

// PUT request with ID
const updatedUser = await apiRequest&lt;User&gt;({
method: 'PUT',
url: '/users',
id: 123,
payload: { name: 'Jane Doe' },
});

// DELETE request
await apiRequest({
method: 'DELETE',
url: '/users',
id: 123,
});

// Legacy API request
const legacyData = await apiRequest({
url: '/legacy-endpoint',
useLegacyAPI: true,
});

Advanced Usage Examples

Employee Management API

import { httpV2 } from '@/api/axios';

class EmployeeAPI {
static async getEmployees(filters = {}) {
const response = await httpV2.get('/employees', {
params: filters,
});
return response.data;
}

static async createEmployee(employeeData) {
const response = await httpV2.post('/employees', employeeData);
return response.data;
}

static async updateEmployee(id, employeeData) {
const response = await httpV2.put(`/employees/${id}`, employeeData);
return response.data;
}

static async deleteEmployee(id) {
await httpV2.delete(`/employees/${id}`);
}

// Bulk operations
static async bulkUpdateEmployees(updates) {
const response = await httpV2.post('/employees/bulk-update', {
updates,
});
return response.data;
}
}

// Usage
const employees = await EmployeeAPI.getEmployees({
department: 'Engineering',
status: 'active',
});

EOR Portal Operations

import { httpV2 } from '@/api/axios';
import { isEorPortalView } from '@/utils/helpers';

class EorAPI {
static async getEorCompanies() {
// EOR parameters automatically injected by interceptor
const response = await httpV2.get('/eor/companies');
return response.data;
}

static async processPayroll(payrollData) {
if (!isEorPortalView()) {
throw new Error('EOR portal access required');
}

const response = await httpV2.post('/eor/payroll/process', payrollData);
return response.data;
}

static async getEmployeesByEorCompany(companyId) {
const response = await httpV2.get('/eor/employees', {
params: { eor_company_id: companyId },
});
return response.data;
}
}

File Upload with Authentication

import { httpV2 } from '@/api/axios';

const uploadFile = async (file, onProgress) => {
const formData = new FormData();
formData.append('file', file);

const response = await httpV2.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: progressEvent => {
const progress = Math.round(
(progressEvent.loaded * 100) / progressEvent.total,
);
onProgress?.(progress);
},
});

return response.data;
};

Custom Hook for API Operations

import { useState, useCallback } from 'react';
import { httpV2 } from '@/api/axios';

const useAPI = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

const request = useCallback(async config => {
setLoading(true);
setError(null);

try {
const response = await httpV2.request(config);
return response.data;
} catch (err) {
setError(err);
throw err;
} finally {
setLoading(false);
}
}, []);

const get = useCallback(
(url, params) => request({ method: 'GET', url, params }),
[request],
);

const post = useCallback(
(url, data) => request({ method: 'POST', url, data }),
[request],
);

const put = useCallback(
(url, data) => request({ method: 'PUT', url, data }),
[request],
);

const del = useCallback(url => request({ method: 'DELETE', url }), [request]);

return {
loading,
error,
get,
post,
put,
delete: del,
request,
};
};

// Usage in component
const EmployeeManager = () => {
const api = useAPI();

const handleCreateEmployee = async employeeData => {
try {
const newEmployee = await api.post('/employees', employeeData);
console.log('Created:', newEmployee);
} catch (error) {
console.error('Creation failed:', error);
}
};

return (
<div>
{api.loading && <div>Loading...</div>}
{api.error && <div>Error: {api.error.message}</div>}
{/* Component content */}
</div>
);
};

Configuration Constants

API Endpoints

// Base URLs from environment variables
export const BASE_API_V1_ENDPOINT = process.env.VITE_API_HOST;
export const BASE_API_V2_ENDPOINT = process.env.VITE_API_HOST_V2;
export const BASE_API_V3_ENDPOINT = process.env.VITE_API_HOST_V3;
export const LEGACY_HOST_ADDRESS = process.env.VITE_LEGACY_API_HOST;

Authentication Tokens

export const USER_TOKEN = userToken();
export const AUTH_COMPANY_ID = authCompanyID();

Error Handling

Global Error Interceptor

// Built into httpV2 instance
httpV2.interceptors.response.use(
response => response,
async error => {
// 401 Unauthorized - Show login modal
if (error?.response?.status === 401) {
if (!isNull(USER_TOKEN)) {
useAuthStore.getState().setOpen(true);
useAuthStore.getState().setLastLoginEmail(currentUser()?.email);
}
}

// Log errors for debugging
console.error('API Error:', {
url: error.config?.url,
method: error.config?.method,
status: error.response?.status,
data: error.response?.data,
});

return Promise.reject(error);
},
);

Custom Error Handling

import { httpV2 } from '@/api/axios';

const safeApiCall = async apiCall => {
try {
return await apiCall();
} catch (error) {
if (error.response?.status === 404) {
console.warn('Resource not found');
return null;
}

if (error.response?.status >= 500) {
console.error('Server error:', error.response.data);
throw new Error('Server error occurred');
}

throw error;
}
};

// Usage
const employee = await safeApiCall(() => httpV2.get(`/employees/${id}`));

Legacy API Support

Legacy Request Configuration

// Enable legacy API for specific requests
httpV2.interceptors.request.use(async config => {
if (config.useLegacyAPI) {
config.baseURL = LEGACY_HOST_ADDRESS;
}
return config;
});

// Usage
const legacyData = await httpV2.get('/legacy-endpoint', {
useLegacyAPI: true,
});

Best Practices

1. Use httpV2 for Standard Operations

// Preferred approach
import { httpV2 } from '@/api/axios';

const fetchData = () => httpV2.get('/endpoint');

2. Handle Authentication Gracefully

const authenticatedRequest = async requestFn => {
try {
return await requestFn();
} catch (error) {
if (error.response?.status === 401) {
// Token might be expired, let interceptor handle it
return null;
}
throw error;
}
};

3. Use TypeScript for Type Safety

interface ApiResponse&lt;T&gt; {
data: T;
message: string;
status: 'success' | 'error';
}

const getUser = async (id: number): Promise&lt;User&gt; => {
const response = await httpV2.get<ApiResponse&lt;User&gt;>(`/users/${id}`);
return response.data.data;
};

4. Implement Request Cancellation

const useApiWithCancellation = () => {
const cancelTokenRef = useRef&lt;AbortController&gt;();

const request = useCallback(async config => {
// Cancel previous request
if (cancelTokenRef.current) {
cancelTokenRef.current.abort();
}

// Create new abort controller
cancelTokenRef.current = new AbortController();

return httpV2.request({
...config,
signal: cancelTokenRef.current.signal,
});
}, []);

useEffect(() => {
return () => {
if (cancelTokenRef.current) {
cancelTokenRef.current.abort();
}
};
}, []);

return { request };
};

5. Cache API Responses

import { httpV2 } from '@/api/axios';

class CachedAPI {
private static cache = new Map();

static async get(url, params = {}, cacheDuration = 5 * 60 * 1000) {
const cacheKey = `${url}${JSON.stringify(params)}`;
const cached = this.cache.get(cacheKey);

if (cached && Date.now() - cached.timestamp < cacheDuration) {
return cached.data;
}

const response = await httpV2.get(url, { params });

this.cache.set(cacheKey, {
data: response.data,
timestamp: Date.now(),
});

return response.data;
}

static clearCache() {
this.cache.clear();
}
}

Performance Considerations

  1. Request Deduplication: Prevent duplicate requests for the same endpoint
  2. Response Caching: Cache frequently accessed data
  3. Request Cancellation: Cancel ongoing requests when components unmount
  4. Batch Operations: Group multiple operations into single requests
  5. Pagination: Implement proper pagination for large datasets


TypeScript Definitions

interface ApiRequestConfig {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
url: string;
id?: number | string;
payload?: any;
query?: Record<string, any>;
useLegacyAPI?: boolean;
responseType?: string;
}

export const http: AxiosInstance;
export const httpV2: AxiosInstance;
export const httpV3: AxiosInstance;

export function apiRequest<T = any>(config: ApiRequestConfig): Promise&lt;T&gt;;
export const defaultAuthAdminParams: Record<string, any>;
export function getDefaultEorPortalParams(): Record<string, any>;

Environment Variables

# API Endpoints
VITE_API_HOST=https://api.workpay.co.ke/api/v1
VITE_API_HOST_V2=https://api.workpay.co.ke/api/v2
VITE_API_HOST_V3=https://api.workpay.co.ke/api/v3
VITE_LEGACY_API_HOST=https://legacy.workpay.co.ke/api

# Authentication
VITE_AUTH_TOKEN_KEY=workpay_auth_token

Dependencies

  • axios: HTTP client library
  • Redux Toolkit: State management for authentication
  • EOR Helpers: For portal-specific filtering
  • Authentication Store: For login state management