axios Best Practices
axios is the go-to HTTP client for modern JavaScript applications due to its robust features and promise-based API. These guidelines ensure your team leverages axios effectively, promoting clean code, centralized logic, and predictable error handling.
1. Centralize Your axios Instance
Always create a single, pre-configured axios instance for your application. This centralizes baseURL, timeout, and default headers, adhering to the DRY principle and simplifying configuration changes.
❌ BAD: Scattered axios calls
// In component A
axios.get('https://api.example.com/users', { timeout: 5000 });
// In component B
axios.post('https://api.example.com/products', data, { headers: { 'Content-Type': 'application/json' } });
✅ GOOD: Dedicated apiClient instance
// src/api/apiClient.js
import axios from 'axios';
const apiClient = axios.create({
baseURL: process.env.REACT_APP_API_BASE_URL || 'https://api.example.com',
timeout: 10000, // 10 seconds
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
});
export default apiClient;
2. Abstract API Endpoints into Modules
Encapsulate each API endpoint or resource into its own module. This promotes the single-responsibility principle, making your API calls testable, reusable, and easy to understand.
❌ BAD: API logic directly in components
// src/components/UserList.jsx
import React, { useEffect, useState } from 'react';
import axios from 'axios'; // Direct axios import
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
axios.get('https://api.example.com/users') // Hardcoded URL
.then(response => setUsers(response.data))
.catch(error => console.error(error));
}, []);
// ...
}
✅ GOOD: Dedicated API service modules
// src/api/users.js
import apiClient from './apiClient'; // Use the centralized instance
export const getUsers = async () => {
const response = await apiClient.get('/users');
return response.data;
};
export const createUser = async (userData) => {
const response = await apiClient.post('/users', userData);
return response.data;
};
// src/components/UserList.jsx
import React, { useEffect, useState } from 'react';
import { getUsers } from '../api/users'; // Import specific API functions
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUsers = async () => {
try {
const data = await getUsers();
setUsers(data);
} catch (err) {
setError('Failed to fetch users.'); // User-friendly error
console.error(err); // Log original error for debugging
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
if (loading) return <div>Loading users...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
3. Leverage Interceptors for Global Logic
Use axios interceptors for cross-cutting concerns like authentication, global error handling, logging, or request/response transformations. This keeps your API functions clean and focused on data fetching.
Request Interceptors (e.g., Auth Tokens)
Inject authentication tokens automatically into every request.
// src/api/apiClient.js (continued)
apiClient.interceptors.request.use(
(config) => {
const token = localStorage.getItem('authToken'); // Or from a state management solution
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
Response Interceptors (e.g., Global Error Handling)
Handle common error statuses (e.g., 401 Unauthorized, 500 Server Error) globally. Map raw errors to user-friendly messages.
// src/api/apiClient.js (continued)
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// Handle 401 Unauthorized globally
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// Example: Redirect to login page or attempt token refresh
console.warn('Unauthorized request, redirecting to login...');
// window.location.href = '/login'; // Or dispatch a global event
return Promise.reject(new Error('Session expired. Please log in again.')); // Return a user-friendly error
}
// Map other HTTP errors to user-friendly messages
let errorMessage = 'An unexpected error occurred.';
if (axios.isAxiosError(error)) {
if (error.response) {
// Server responded with a status other than 2xx
errorMessage = error.response.data?.message || `Server Error: ${error.response.status}`;
} else if (error.request) {
// Request was made but no response received
errorMessage = 'No response from server. Please check your internet connection.';
} else {
// Something else happened while setting up the request
errorMessage = error.message;
}
} else {
errorMessage = error.message || 'An unknown error occurred.';
}
console.error('API Error:', error); // Log full error for debugging
return Promise.reject(new Error(errorMessage)); // Propagate user-friendly error
}
);
4. Implement Request Cancellation with AbortController
Prevent memory leaks and unnecessary network activity, especially in React components that might unmount before a request completes. Always use AbortController for cancellation; CancelToken is deprecated.
❌ BAD: Ignoring cancellation or using deprecated CancelToken
// In a React component
useEffect(() => {
axios.get('/long-running-data').then(...); // Request might complete after component unmounts
}, []);
// Using deprecated CancelToken
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/data', { cancelToken: source.token });
source.cancel('Operation cancelled.');
✅ GOOD: Using AbortController in useEffect cleanup
// src/components/DataFetcher.jsx
import React, { useEffect, useState } from 'react';
import apiClient from '../api/apiClient';
import axios from 'axios'; // Import axios to use axios.isCancel
function DataFetcher() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController(); // Create AbortController
const signal = controller.signal;
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await apiClient.get('/some-data', { signal }); // Pass signal to axios
setData(response.data);
} catch (err) {
if (axios.isCancel(err)) {
console.log('Request cancelled:', err.message);
} else {
setError(err.message); // Use the user-friendly message from interceptor
console.error('Fetch error:', err);
}
} finally {
setLoading(false);
}
};
fetchData();
return () => {
controller.abort('Component unmounted'); // Abort request on cleanup
};
}, []);
if (loading) return <div>Loading data...</div>;
if (error) return <div>Error: {error}</div>;
return <div>{JSON.stringify(data)}</div>;
}
5. Validate Payloads Before Sending
Always validate request payloads on the client-side before sending them to the server. This reduces unnecessary network requests and provides immediate feedback to the user.
❌ BAD: Sending potentially invalid data
const handleSubmit = async (formData) => {
// No client-side validation
await createUser(formData);
};
✅ GOOD: Client-side validation
import { createUser } from '../api/users';
const handleSubmit = async (formData) => {
if (!formData.name || formData.name.length < 3) {
alert('Name must be at least 3 characters.');
return;
}
if (!formData.email || !/^\S+@\S+\.\S+$/.test(formData.email)) {
alert('Please enter a valid email address.');
return;
}
try {
await createUser(formData);
alert('User created successfully!');
} catch (error) {
alert(`Failed to create user: ${error.message}`); // Display user-friendly error
}
};
6. Use TypeScript for API Contracts
Define explicit TypeScript interfaces for your request payloads and response data. This improves maintainability, provides compile-time safety, and enhances developer experience with autocompletion.
// src/types/api.ts
export interface User {
id: number;
name: string;
email: string;
}
export interface CreateUserPayload {
name: string;
email: string;
password?: string;
}
// src/api/users.ts (example with TypeScript)
import apiClient from './apiClient';
import { User, CreateUserPayload } from '../types/api';
export const getUsers = async (): Promise<User[]> => {
const response = await apiClient.get<User[]>('/users');
return response.data;
};
export const createUser = async (userData: CreateUserPayload): Promise<User> => {
const response = await apiClient.post<User>('/users', userData);
return response.data;
};
7. Test API Interactions with Mocking
For unit and integration tests, mock axios requests to avoid making actual network calls. This makes tests faster, more reliable, and independent of external API availability. axios-mock-adapter is a common choice.
// src/api/users.test.js
import MockAdapter from 'axios-mock-adapter';
import apiClient from './apiClient';
import { getUsers, createUser } from './users';
const mock = new MockAdapter(apiClient);
describe('users API', () => {
afterEach(() => {
mock.reset(); // Reset mocks after each test
});
it('should fetch users successfully', async () => {
const mockUsers = [{ id: 1, name: 'Alice', email: 'alice@example.com' }];
mock.onGet('/users').reply(200, mockUsers);
const users = await getUsers();
expect(users).toEqual(mockUsers);
});
it('should create a user successfully', async () => {
const newUserPayload = { name: 'Bob', email: 'bob@example.com' };
const createdUser = { id: 2, ...newUserPayload };
mock.onPost('/users').reply(201, createdUser);
const user = await createUser(newUserPayload);
expect(user).toEqual(createdUser);
});
it('should handle API errors gracefully', async () => {
mock.onGet('/users').reply(500, { message: 'Internal Server Error' });
await expect(getUsers()).rejects.toThrow('Server Error: 500'); // Based on interceptor error message
});
});