In previous articles, we learned how to request data using fetch. It works, it’s native, and it’s standard.
But as we saw, it has its rough edges: you have to manually parse the JSON, it doesn’t handle HTTP errors automatically, and it forces us to repeat headers constantly.
When building large applications, sometimes we need a more comfortable layer. A very common option (though not mandatory) is Axios.
Axios is a promise-based library that does the same thing as fetch, but with a more convenient API and advanced features that save us a lot of repetitive code.
Native fetch has improved a lot in recent years, and continues to do so. This means Axios is no longer as essential as it once was.
Even so, it remains a widely used option, and you will definitely come across it. So let’s take a look at it.
Why Axios?
Before installing anything, let’s understand the immediate improvements over fetch:
- Automatic JSON transformation: Say goodbye to
response.json(). Axios returns the data directly. - Sensible error handling: If the API returns a 404 or a 500, Axios automatically throws an exception (enters the
.catch). Withfetch, we had to checkresponse.okmanually. - Instances and Interceptors: The main reason many people use it.
Installation
Installing Axios is very simple. It is installed just like any other normal package, no mystery involved.
npm install axios
Creating an Instance (Singleton)
A common mistake is to import axios directly into each component (import axios from 'axios').
If you do that, you will have to write the base URL (https://api.mydomain.com) in every request. If the API address changes tomorrow, you will have to edit 50 files.
To avoid this repetition, we will create a Centralized Instance.
Create a file at src/api/axios.js (or src/lib/axios.js):
import axios from 'axios';
// Create an instance with base configuration
const api = axios.create({
baseURL: 'https://api.yourwebsite.com/v1',
timeout: 5000, // If it takes longer than 5s, abort
headers: {
'Content-Type': 'application/json'
}
});
export default api;
Now, in your components, you will import this api object instead of the axios library.
// In any component
import api from '../api/axios';
// The request will be to: https://api.yourwebsite.com/v1/users
const response = await api.get('/users');
Interceptors
Interceptors are functions that run before a request leaves our app, or before a response reaches our component.
They are like customs checkpoints or tolls through which all HTTP traffic passes.
Request Interceptor
The number one use case is Authentication. If we have a user token (JWT) saved in LocalStorage or a Zustand Store, we want to send it with every private request.
Instead of adding the Authorization header manually in each call, we let the interceptor inject it.
// src/api/axios.js (continued)
api.interceptors.request.use(
(config) => {
// 1. Look for the token (in localStorage or wherever you have it)
const token = localStorage.getItem('token');
// 2. If it exists, inject it into the header
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config; // Important to return the modified config
},
(error) => {
return Promise.reject(error);
}
);
Now any call like api.get('/profile') will automatically carry the token.
Response Interceptor
The primary use case here is Global Error Handling. What happens if the token has expired (Error 401)? We don’t want to check for error 401 in every component.
We want that if a 401 occurs anywhere, the app automatically redirects the user to the Login page.
api.interceptors.response.use(
(response) => {
// If the response is correct, simply return the data
// This saves us from writing .data in every component
return response.data;
},
(error) => {
// If the response is an error...
if (error.response) {
// 1. Expired or invalid token
if (error.response.status === 401) {
localStorage.removeItem('token');
window.location.href = '/login'; // Forced redirect
}
// 2. Server error
if (error.response.status === 500) {
console.error("The server has crashed!");
}
}
return Promise.reject(error);
}
);
Refactoring our Component
Let’s see how our UserList component from the previous article looks after applying this architecture.
import { useState, useEffect } from "react";
import api from "../api/axios"; // Import our instance
export default function UserList() {
const [users, setUsers] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
// 1. Cleaner syntax
// 2. No need to include the full URL
// 3. No need for .json() (the interceptor already returned response.data)
const data = await api.get("/users");
setUsers(data);
} catch (err) {
// Axios gives us a detailed error message
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
);
}
Conclusion
Using an Axios instance with interceptors is one of those decisions you appreciate as the project grows. It centralizes network logic, simplifies components, and makes maintenance more manageable.
With this, we now know how to request data. But sometimes, we perform heavy calculations on that data or render gigantic lists that slow down the app. It’s time to enter the final phase of the course: Optimization and Performance.
Next step in the course:
