Axios is a well-known HTTP client that makes it easy for React apps to talk to APIs. Axios is a fantastic way to make the native fetch API easier to use or to make error handling cleaner. This guide tells you everything you need to know about using Axios well in your projects.
What is Axios?
Axios is a library that lets you make HTTP requests based on promises. It works in both Node.js and web browsers. The library has become the go-to choice for React developers because it simplifies common tasks that would otherwise require more code with the native fetch API.
The main difference between Axios and fetch is how easy they are for developers to use. Axios automatically turns response data into JSON, has built-in error handling for HTTP status codes, and comes with features like request interceptors. You would have to parse JSON and check response status codes by hand if you used fetch.
Axios also works with older browsers that don't have polyfills, so it's a good choice for projects that need to work with many different browsers. The library is about 13KB, which is a fair trade-off for how useful it is.
Installation
You only need one command to add Axios to your React project. In the project directory, open your terminal and type:
npm install axios
Or using yarn:
yarn add axios
You can import Axios into any part of your app after you install it. The library exports a default instance that works right away, but most projects are better off making their own configuration.
Setting Up an Axios Instance
You can use Axios directly, but making a configured instance helps keep your code neat. You can set a base URL, default headers, and timeout settings all in one place with an instance. These settings are automatically passed on to every request made through that instance.
import axios from 'axios';
export const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
headers: {
'Content-Type': 'application/json',
},
timeout: 10000,
});
Don't hardcode your API URL; instead, put it in an environment file. This method makes it easy to switch between development and production environments. Make a .env file in the root of your project and put your API address in it.
VITE_API_URL=https://api.example.com
After you've set it up, use this instance in your app instead of the default Axios. This method makes sure that all API calls behave the same way and makes it easier to make changes in the future.
Making GET Requests
GET requests get data from a server. When a component mounts in React, you usually make these requests inside a useEffect hook. Axios gives you a promise that resolves with a response object that has your data in it.
useEffect(() => {
const fetchUsers = async () => {
try {
const { data } = await api.get('/users');
setUsers(data);
} catch (error) {
console.error('Could not load users');
}
};
fetchUsers();
}, []);
There are several properties in the Axios response object. The data property has the actual response payload. You can also see the HTTP code status, response headers, and request config.
You can send query parameters through the params option. You don't have to make query strings by hand because Axios does URL encoding for you.
const { data } = await api.get('/users', {
params: {
page: 1,
limit: 10,
search: 'john',
},
});
Sending POST Requests
POST requests send data to a server, usually to make new resources. Please provide your data as the second argument to the post method. Axios automatically changes JavaScript objects into JSON and adds the right content type header.
const handleSubmit = async (formData: CreateUserPayload) => {
try {
const { data } = await api.post('/users', formData);
return data;
} catch (error) {
console.error('Failed to create user');
}
};
For file uploads, use FormData instead of a regular object. Set the content type to multipart/form-data in the request settings.
const uploadFile = async (file: File) => {
const formData = new FormData();
formData.append('file', file);
const { data } = await api.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: progressEvent => {
const percent = Math.round((progressEvent.loaded * 100) / (progressEvent.total ?? 1));
console.log(`Upload progress: ${percent}%`);
},
});
return data;
};
You can use the onUploadProgress callback in Axios to keep track of how far along an upload is. This is useful for showing progress bars when uploading large files.
PUT, PATCH, and DELETE Requests
Updating and deleting resources works the same way as GET and POST. To replace a whole resource, use api.put(). To update part of it, use api.patch(). To delete it, use api.delete(). Each method needs an endpoint URL and may also need data or settings.
// Replace entire resource
const updateUser = async (id: number, userData: User) => {
const { data } = await api.put(`/users/${id}`, userData);
return data;
};
// Update specific fields only
const updateUserEmail = async (id: number, email: string) => {
const { data } = await api.patch(`/users/${id}`, { email });
return data;
};
// Delete resource
const deleteUser = async (id: number) => {
await api.delete(`/users/${id}`);
};
Depending on how you design your API, you can choose between PUT and PATCH. PUT usually wants a full resource representation, but PATCH lets you send only the fields that changed. Most modern APIs work with both methods.
Handling Errors
Axios has better structured error handling than fetch does. When a request fails, Axios rejects the promise and sends back an error object with a lot of information about what went wrong.
The error object has a response property for server errors (4xx and 5xx status codes), a request property for when there was no response, and a message property for other types of errors. This structure lets you give the right feedback for different kinds of mistakes.
try {
const { data } = await api.get('/users');
return data;
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response) {
// Server responded with error status (4xx, 5xx)
console.error('Server error:', error.response.status);
console.error('Error data:', error.response.data);
} else if (error.request) {
// Request was made but no response received
console.error('Network error');
} else {
// Something else went wrong
console.error('Error:', error.message);
}
}
throw error;
}
Your app will handle failures better if you put API calls in try-catch blocks. When something goes wrong, users should always get feedback instead of seeing a frozen interface.
Using Interceptors
You can run code interceptors either before requests leave your app or after responses come in. The most common use case is to automatically add authentication tokens to every request.
Request interceptors get the configuration object before the request is sent. You can change headers, log requests, or change the data. Response interceptors take care of the response before it gets to your code. This function is useful for fixing mistakes or changing data on a large scale.
// Request interceptor - add auth token
api.interceptors.request.use(config => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor - handle errors globally
api.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
localStorage.removeItem('auth_token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
Once you set up interceptors, you never have to add tokens to individual requests by hand again. Your component code can focus on business logic because the interceptor handles authentication in the background.
Cancelling Requests
React components can unmount even if there are still requests waiting. If the response comes after unmounting, trying to update a state will cause warnings and possible memory leaks. The AbortController API lets you cancel requests in Axios.
At the beginning of your effect, make an AbortController and send its signal to the request. When the component unmounts, call abort in the cleanup function to cancel any requests that are still open.
useEffect(() => {
const controller = new AbortController();
api
.get('/users', { signal: controller.signal })
.then(({ data }) => setUsers(data))
.catch(err => {
if (!axios.isCancel(err)) {
console.error(err);
}
});
return () => controller.abort();
}, []);
The axios.isCancel() helper tells you when a cancellation was planned so you don't have to treat it like an error. This pattern is necessary for any part that gets data when it mounts.
Organising API Calls with Services
Keeping track of API calls across different parts of your application can be challenging. A service layer pattern puts all the API calls for a resource in one file. Each service exports functions that components can use without knowing how they work.
// services/users.service.ts
import { api } from '@/config/axios.config';
export default class UsersService {
static async getAll(): Promise<User[]> {
const { data } = await api.get('/users');
return data;
}
static async getById(id: number): Promise<User> {
const { data } = await api.get(`/users/${id}`);
return data;
}
static async create(payload: CreateUserPayload): Promise<User> {
const { data } = await api.post('/users', payload);
return data;
}
static async update(id: number, payload: UpdateUserPayload): Promise<User> {
const { data } = await api.put(`/users/${id}`, payload);
return data;
}
static async delete(id: number): Promise<void> {
await api.delete(`/users/${id}`);
}
}
Components bring in the service and use its methods. This separation makes testing easier, cuts down on code duplication, and gives you one place to update when API endpoints change. For bigger apps, the pattern works well with state management solutions.
Making Requests in Parallel
You may need data from more than one endpoint before you can render a component. With Promise.all, you can send out multiple requests at the same time and wait for all of them to finish. This method is faster than making requests one at a time.
const fetchDashboardData = async () => {
const [usersRes, ordersRes, statsRes] = await Promise.all([api.get('/users'), api.get('/orders'), api.get('/stats')]);
return {
users: usersRes.data,
orders: ordersRes.data,
stats: statsRes.data,
};
};
If one request fails, Promise.allSettled should be used instead. It ends when all promises are fulfilled, whether they were successful or not.
const results = await Promise.allSettled([api.get('/users'), api.get('/orders'), api.get('/stats')]);
const data = {
users: results[0].status === 'fulfilled' ? results[0].value.data : [],
orders: results[1].status === 'fulfilled' ? results[1].value.data : [],
stats: results[2].status === 'fulfilled' ? results[2].value.data : null,
};
Professional development teams use these patterns to create responsive interfaces.
Best Practices
There are a few things you can do to get the most out of Axios in React apps.
Always create a configured instance instead of using the default Axios. This keeps your headers and base URL together. Put configuration values in environment variables so you can change them without having to change the code.
Cancel requests when components unmount. This stops memory leaks and warnings in the console about updating components that aren't mounted. Anyone who writes code to obtain data should be able to use the AbortController pattern without contemplating it.
Handle errors at every level. Interceptors catch problems that affect everyone, like failed logins. Individual requests should still have try-catch blocks to deal with certain types of errors and provide feedback to users.
Organise API calls into service files as your app grows. This pattern cuts down on duplicate code and makes the codebase easier to find your way around. You only have to update one file when an endpoint changes instead of looking through all the components.
Don't hardcode URLs. Environment variables make your app more flexible and stop you from accidentally sending requests to the wrong servers.
When to Pick Axios Over Fetch
Axios is excellent when you need interceptors, automatic JSON handling, or a way to cancel requests with cleaner syntax. The library takes care of the boilerplate code that fetch needs, so you can focus on what matters.
Fetch is fine for simple apps that don't need to use the API much. There is no need to install it, and it doesn't change the size of the bundle. If you only need to make occasional requests and don't need advanced features, fetch may suffice.
Most professional projects benefit from Axios because the ease of use outweighs the small increase in bundle size. Axios is the recommended option for building applications that need strong API communication. It has worked well on many client projects and is still being actively maintained.








