Why Axios? Comparing Axios and the Fetch API

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
Note: The most critical Fetch vs Axios difference is error handling. Fetch only rejects its Promise for network failures (no internet, DNS error). A 404 or 500 response from your Express server is considered a “successful” fetch — the Promise resolves. You must manually check 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.
Tip: Even if you prefer Fetch for simple scripts, the interceptor system alone makes Axios worth it for MERN applications. Adding a JWT token to every request, or globally handling token expiry by clearing localStorage and redirecting to login — these require one setup in an Axios interceptor vs a wrapper function or custom hook in every component with Fetch.
Warning: Never put your Axios base URL or any API URL directly as a string in multiple component files. If your API URL ever changes (e.g. when moving from development to production), you would have to update it in dozens of places. Always create a single configured Axios instance (covered in Lesson 2) and import it wherever you need to make API calls.

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)

🧠 Test Yourself

You call fetch('/api/posts/invalid-id') and the server responds with a 404 status and body { message: 'Not found' }. Your code then calls const data = await res.json() and sets the posts state. What gets stored in the posts state?