DEV Community

Mohamed Idris
Mohamed Idris

Posted on

Axios: A Simple, Practical Guide (with Examples)

A big part of most modern frontend projects is fetching data from APIs and sending data back to a server. While you can do everything with the browser’s built-in fetch() API, many teams choose Axios to avoid common headaches and keep the workflow smoother.

Axios is:

  • Not part of React (works with any framework or vanilla JS)
  • A very popular HTTP client library
  • Built around promises, with a clean API and great defaults

Docs: Axios Docs


Install Axios

NPM

npm install axios
Enter fullscreen mode Exit fullscreen mode

CDN (quick demos)

<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
Enter fullscreen mode Exit fullscreen mode

Your First Request

Axios provides methods for the common HTTP verbs:

  • axios.get(url)
  • axios.post(url, data)
  • axios.put(url, data) / axios.patch(url, data)
  • axios.delete(url)

Also, calling axios(url) defaults to a GET request.

Key notes:

  • Axios returns a promise
  • The response body is in response.data
  • Errors often include server details in error.response
import axios from "axios";

const url = "https://api.example.com/users";

const fetchData = async () => {
  try {
    // axios(url) defaults to GET
    const response = await axios(url);

    console.log("Full response:", response);
    console.log("Data only:", response.data);
  } catch (error) {
    console.log("Error response:", error.response);
  }
};

fetchData();
Enter fullscreen mode Exit fullscreen mode

GET with Headers

Axios accepts a config object as the second argument in get():

const fetchDadJoke = async () => {
  const url = "https://icanhazdadjoke.com/";

  try {
    const { data } = await axios.get(url, {
      headers: {
        Accept: "application/json",
      },
    });

    console.log("Joke:", data.joke);
  } catch (error) {
    console.log(error.response);
  }
};

fetchDadJoke();
Enter fullscreen mode Exit fullscreen mode

Why this matters: some APIs return HTML by default unless you explicitly ask for JSON.


POST Request (Sending Data)

To send data to the server, use axios.post(url, data).

  • Second argument is your payload
  • Third argument is config (headers, auth, etc.)
import axios from "axios";

const url = "https://api.example.com/posts";

const createPost = async () => {
  try {
    const payload = { title: "Hello Axios", body: "This is a post." };
    const resp = await axios.post(url, payload);

    console.log("Created:", resp.data);
  } catch (error) {
    console.log("Server error:", error.response?.data);
  }
};

createPost();
Enter fullscreen mode Exit fullscreen mode

Global Defaults (Convenient, But Use Carefully)

Axios lets you set defaults once, so you don’t repeat config everywhere.

import axios from "axios";

axios.defaults.baseURL = "https://api.example.com";
axios.defaults.headers["Accept"] = "application/json";

// Example: attach a token (if you have one)
const AUTH_TOKEN = "YOUR_TOKEN_HERE";
axios.defaults.headers["Authorization"] = `Bearer ${AUTH_TOKEN}`;

// Example: set default content-type for POST requests
axios.defaults.headers.post["Content-Type"] = "application/x-www-form-urlencoded";
Enter fullscreen mode Exit fullscreen mode

Note: In newer Axios versions, axios.defaults.headers.common may not behave like older tutorials. Setting headers via axios.defaults.headers[...] is a safer pattern.


Create a Custom Axios Instance (Recommended)

Instead of modifying global defaults, you can create an Axios instance for a specific API.

import axios from "axios";

const authFetch = axios.create({
  baseURL: "https://www.course-api.com",
  headers: {
    Accept: "application/json",
  },
});

// Usage
const getProducts = async () => {
  const { data } = await authFetch.get("/react-store-products");
  console.log(data);
};

getProducts();
Enter fullscreen mode Exit fullscreen mode

This keeps your codebase cleaner—especially when you have multiple APIs or different auth rules.


Interceptors (Power Feature)

Interceptors let you run logic before a request is sent and after a response is received.

Typical uses:

  • Automatically attach auth tokens
  • Log requests
  • Handle global errors like 401/404
authFetch.interceptors.request.use(
  (request) => {
    request.headers["Accept"] = "application/json";

    console.log("Request sent:", request.method?.toUpperCase(), request.url);
    return request; // must return the request
  },
  (error) => Promise.reject(error), // Keep the promise in a rejected state so the caller's catch/try-catch still runs.
);

authFetch.interceptors.response.use(
  (response) => {
    console.log("Response received:", response.status);
    return response;
  },
// Error handler for the response interceptor.
// This runs when Axios gets a non-2xx response (like 404/401/500) or a network failure.
// `error.response` (when it exists) contains the server response details: status, headers, data, etc.
  (error) => {
    console.log("Response error:", error.response);

    // Example: handle "Not Found" globally in one place.
  // Useful for showing a toast, redirecting to a 404 page, or tracking broken links.
  if (error.response?.status === 404) {
    console.log('NOT FOUND');
    // you could redirect, show toast, etc.
  }

  // Example: if it's 401 (Unauthorized), you usually log the user out / clear the token.
  // if (error.response?.status === 401) logoutUser();

  // IMPORTANT:
  // `Promise.reject(error)` re-throws the error as a rejected promise.
  // That means the calling code still sees this request as "failed" and its `.catch(...)`
  // (or try/catch around `await`) will run. Without rejecting, Axios would treat it like a success.
  return Promise.reject(error);
  }
);
Enter fullscreen mode Exit fullscreen mode

Quick Mental Model: Axios vs Fetch

Why people often prefer Axios:

  • response.data gives you parsed data directly
  • ✅ Automatic JSON transformation (most cases)
  • ✅ Better ergonomics around headers/config
  • ✅ Built-in interceptors
  • ✅ Cleaner error handling via error.response

Fetch is still totally valid—Axios just tends to reduce friction in real projects.


Wrap-up

If your upcoming project involves:

  • lots of API calls,
  • consistent headers/tokens,
  • centralized error handling,

…Axios will likely make your life easier. Start simple with axios.get() / axios.post(), then scale into instances and interceptors as the app grows.

Top comments (2)

Collapse
 
edriso profile image
Mohamed Idris

Common Axios mistakes

  1. Forgetting response.data Axios wraps the payload, so you usually want:
const { data } = await axios.get(url);
Enter fullscreen mode Exit fullscreen mode

—not response itself.

  1. Mixing up GET vs POST arguments
  • get(url, config)
  • post(url, data, config) A super common bug is accidentally sending headers as the “data” argument in post().
  1. Using fetch-style error checks With fetch, you check res.ok. With Axios, non-2xx responses throw, so handle it in catch:
catch (err) {
  console.log(err.response?.status);
  console.log(err.response?.data);
}
Enter fullscreen mode Exit fullscreen mode
  1. Reading the wrong error field Network error (no response) vs server error (has response):
if (!err.response) console.log("Network/CORS issue");
Enter fullscreen mode Exit fullscreen mode
  1. Setting defaults globally and forgetting later axios.defaults... affects everything. Prefer a custom instance for APIs/auth:
const api = axios.create({ baseURL });
Enter fullscreen mode Exit fullscreen mode
  1. Old tutorials using headers.common Some newer Axios setups don’t behave like older examples. Safer pattern:
axios.defaults.headers["Authorization"] = token;
Enter fullscreen mode Exit fullscreen mode
  1. Forgetting to return in interceptors
    If you don’t return request / return response, things break silently.

  2. Not cancelling requests on unmount (React)
    Can cause “state update on unmounted component” warnings. Use AbortController or your preferred pattern.

Collapse
 
edriso profile image
Mohamed Idris

About point #8 in the previous comment, it’s not just old class-based React — it can happen in both class components and functional components.

Why it happens

When you start a request and the component unmounts before the request finishes, the promise can still resolve later. If you then do something like setState(...) (class) or setX(...) (hooks), you’re trying to update UI that no longer exists.

  • Class component: this.setState(...) after unmount
  • Functional component: setSomething(...) after unmount (from useState)

Historically this produced the classic warning: “Can’t perform a React state update on an unmounted component.”
(React has evolved and the exact warning behavior depends on version/dev mode, but the underlying issue is still real: wasted work + possible memory leaks + weird UI timing.)


Functional component pattern: AbortController (recommended)

Axios supports aborting via signal (modern versions):

import { useEffect, useState } from "react";
import axios from "axios";

function Users() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    const controller = new AbortController();

    const load = async () => {
      try {
        const { data } = await axios.get("/api/users", {
          signal: controller.signal,
        });
        setUsers(data);
      } catch (err) {
        // Ignore cancellations
        if (err.name === "CanceledError") return;
        console.error(err);
      }
    };

    load();

    return () => controller.abort(); // cancel on unmount
  }, []);

  return <pre>{JSON.stringify(users, null, 2)}</pre>;
}
Enter fullscreen mode Exit fullscreen mode

Notes

  • The cleanup function in useEffect runs on unmount, so that’s where you abort.
  • If you’re using older Axios examples, you might see CancelToken — it’s largely legacy compared to AbortController.

Alternative: “isMounted” flag (works, but less ideal)

This doesn’t cancel the request, it just prevents state updates:

useEffect(() => {
  let mounted = true;

  axios.get("/api/users").then((res) => {
    if (mounted) setUsers(res.data);
  });

  return () => {
    mounted = false;
  };
}, []);
Enter fullscreen mode Exit fullscreen mode

This avoids setting state after unmount, but the request still runs.


So the answer

It applies to functional components too.
In modern React apps, you’ll mostly hit this with hooks + async calls inside useEffect, event handlers that trigger requests, or route changes/unmounts.