Every time the MERN Blog loads posts, submits a form, or deletes a resource, React makes an HTTP request to your Express API. While the browser’s native Fetch API can do this, Axios is the HTTP client used in virtually every production MERN application. The reasons are practical: Axios automatically parses JSON, throws errors for non-2xx responses, supports interceptors for auth headers and error handling, and integrates cleanly with async/await. In this lesson you will understand exactly where Axios improves on Fetch and why the difference matters for building a maintainable MERN application.
Fetch vs Axios — Direct Comparison
| Concern | Native Fetch | Axios |
|---|---|---|
| JSON request body | JSON.stringify(data) + Content-Type header manually |
Automatic — pass object, Axios handles it |
| JSON response parsing | Two-step: await res.json() |
Automatic — data is in res.data |
| Error on non-2xx | Does NOT throw — must check res.ok manually |
Throws automatically for 4xx and 5xx |
| Request interceptors | No built-in support | First-class feature — add auth headers globally |
| Response interceptors | No built-in support | Handle 401/500 globally in one place |
| Request cancellation | AbortController (manual) | AbortController via signal (same API) |
| Upload progress | Not available | onUploadProgress callback |
| Base URL | Must repeat in every call | Set once on instance: baseURL |
| Default timeout | None (hangs forever) | Configurable: timeout: 10000 |
response.ok and throw. Axios throws for any non-2xx response automatically, which means your catch block handles both network errors and API errors consistently.The Fetch Problem — Manual Error Handling
// Using native Fetch — every call site needs boilerplate
const fetchPosts = async () => {
try {
const response = await fetch('/api/posts');
// Fetch does NOT throw on 4xx/5xx — must check manually
if (!response.ok) {
const errorData = await response.json(); // parse error body
throw new Error(errorData.message || `HTTP ${response.status}`);
}
const data = await response.json(); // parse success body
setPosts(data.data);
} catch (err) {
setError(err.message);
}
};
// Same code with JSON POST body — more boilerplate
const createPost = async (postData) => {
const response = await fetch('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`,
},
body: JSON.stringify(postData), // must stringify manually
});
if (!response.ok) { ... }
return response.json();
};
Axios — Clean and Consistent
import axios from 'axios';
// Axios: JSON parsed automatically, errors thrown automatically
const fetchPosts = async () => {
try {
const res = await axios.get('/api/posts');
setPosts(res.data.data); // res.data is already parsed
} catch (err) {
// err.response contains the parsed Express error
setError(err.response?.data?.message || 'Failed to load posts');
}
};
// POST with JSON body — Axios handles Content-Type automatically
const createPost = async (postData) => {
const res = await axios.post('/api/posts', postData); // object → JSON automatically
return res.data.data;
};
// The error object structure from Axios
// err.message → 'Request failed with status code 401'
// err.response → the full response object
// err.response.status → 401
// err.response.data → { success: false, message: 'No token provided' }
// err.request → the request that was made (if no response received)
// axios.isCancel(err) → true if request was cancelled by AbortController
Axios Installation and Import
cd client
npm install axios
import axios from 'axios'; // named or default import
// Named request methods
axios.get(url, config)
axios.post(url, data, config)
axios.put(url, data, config)
axios.patch(url, data, config)
axios.delete(url, config)
// Generic method
axios.request({ method: 'get', url: '/api/posts' })
Axios Request Configuration
// Every Axios method accepts a config object as the last argument
const res = await axios.get('/api/posts', {
params: { page: 2, limit: 10, tag: 'mern' }, // added as query string
headers: { Authorization: 'Bearer token' },
timeout: 10000, // 10 second timeout
signal: controller.signal, // AbortController
});
// axios.post takes data as second arg, config as third
await axios.post('/api/posts', postData, {
headers: { Authorization: 'Bearer token' },
onUploadProgress: (e) => {
const pct = Math.round((e.loaded * 100) / e.total);
setUploadProgress(pct);
},
});
Common Mistakes
Mistake 1 — Treating a 404 from Fetch as a success
❌ Wrong — Fetch does not throw on 404:
const res = await fetch('/api/posts/nonexistent-id');
const data = await res.json(); // { success: false, message: 'Post not found' }
setPosts(data); // ← 404 error treated as success, data set to error object!
✅ Correct — always check res.ok with Fetch, or use Axios which throws automatically:
const res = await axios.get('/api/posts/nonexistent-id');
// Throws automatically with 404 → caught by catch block ✓
Mistake 2 — Accessing res.data.data vs res.data
❌ Wrong — not accounting for the Express API response envelope:
const res = await axios.get('/api/posts');
setPosts(res.data); // res.data = { success: true, data: [...], total: 47 }
// posts is now the whole envelope object, not the array!
✅ Correct — access the nested data property matching your Express response shape:
const res = await axios.get('/api/posts');
setPosts(res.data.data); // ✓ the actual array of posts
Mistake 3 — Not cancelling Axios requests on cleanup
❌ Wrong — request completes after unmount and updates state:
useEffect(() => {
axios.get('/api/posts').then(res => setPosts(res.data.data));
// No cleanup — if component unmounts before response → state update on dead component
}, []);
✅ Correct — cancel in the cleanup function:
useEffect(() => {
const controller = new AbortController();
axios.get('/api/posts', { signal: controller.signal })
.then(res => setPosts(res.data.data))
.catch(err => { if (!axios.isCancel(err)) setError(err.message); });
return () => controller.abort(); // ✓
}, []);
Quick Reference
| Task | Axios Code |
|---|---|
| GET request | await axios.get('/api/posts') |
| GET with query params | await axios.get('/api/posts', { params: { page: 2 } }) |
| POST with JSON body | await axios.post('/api/posts', postData) |
| PATCH request | await axios.patch(`/api/posts/${id}`, updates) |
| DELETE request | await axios.delete(`/api/posts/${id}`) |
| Access response data | res.data (already parsed JSON) |
| Access error message | err.response?.data?.message |
| Check cancellation | axios.isCancel(err) |