JavaScript and React course Β· Module 11: Data Fetching and APIs

Axios and HTTP Libraries

7 min read
In this lesson7

Your ship already sends requests to Mission Control with fetch(), but you quickly notice the repetitive work: for every request you check response.ok by hand, parse JSON by hand, and retype the same server address and the same token in every file. The built-in fetch() is a basic radio transmitter. As the fleet grows, we need a communication system with filters, a time limit and a single configuration center. That's when we reach for Axios, one of the most popular HTTP libraries for JavaScript (the current line is Axios 1.x).

Why Axios Instead of fetch?

The easiest way to see the difference is the same request written two ways. Notice how many lines fetch() needs to behave safely:

1// FETCH - requires manual handling
2const response = await fetch(url);
3if (!response.ok) throw new Error('Error'); // fetch does NOT throw errors for 4xx/5xx!
4const data = await response.json(); // must manually parse JSON
5
6// AXIOS - simpler and safer
7const { data } = await axios.get(url); // automatic JSON, throws errors for 4xx/5xx

Both versions fetch the same data, and the server sees no difference at all. The only thing that changes is how much work you do: fetch() rejects its Promise only on a network failure, so you have to catch a 404 yourself, while Axios by default treats every status outside the 2xx range as an error.

The key differences in one table:

Featurefetch()Axios
JSON parsingManual (.json())Automatic
HTTP errorsDoesn't throw for 4xx/5xxThrows automatically
InterceptorsNoneBuilt-in
TimeoutNo option, but there is AbortSignal.timeout(ms)timeout option
CancellationAbortControllerAbortController (signal), CancelToken is deprecated
TransformationManualBuilt-in

Installing and Using Axios

You install Axios like any package and then call methods named after the HTTP methods. The response object has a data field with ready-to-use data, status with the code and headers with the headers:

1// Installation: npm install axios
2
3import axios from 'axios';
4
5// GET - fetch data
6const response = await axios.get(
7  'https://api.space-center.com/planets'
8);
9console.log(response.data);   // data (already parsed!)
10console.log(response.status); // 200
11console.log(response.headers); // headers
12
13// POST - send data (automatic JSON serialization)
14const newMission = await axios.post(
15  'https://api.space-center.com/missions',
16  { name: 'Alpha Centauri', crew: 5 }
17);
18
19// PUT, DELETE
20await axios.put(url, updatedData);
21await axios.delete(url);

With axios.post you pass a plain object, and Axios turns it into JSON and sets the Content-Type header itself. The server address, however, is still repeated on every line, and that's what we fix next.

Creating an API Instance

The axios.create() method creates a separate client with its own configuration: a base address (baseURL), a time limit in milliseconds (timeout) and default headers. It's like tuning the radio to Mission Control's fixed frequency:

1// Create an API client with default configuration
2const spaceApi = axios.create({
3  baseURL: 'https://api.space-center.com',
4  timeout: 10000, // 10 seconds
5  headers: {
6    'Content-Type': 'application/json',
7    'X-Station-Id': 'ISS-2150',
8  },
9});
10
11// Now we use shorter paths
12const planets = await spaceApi.get('/planets');
13const missions = await spaceApi.get('/missions');
14const newMission = await spaceApi.post('/missions', {
15  name: 'Deep Space Survey',
16  destination: 'Proxima Centauri',
17});

From now on short paths like /planets are enough. The global axios object has not changed, so other parts of the app can have their own instances for other servers. My recommendation: one instance per API, kept in its own file.

Interceptors - Communication Filters

An interceptor is a function that Axios runs for every request before it leaves, or for every response before it reaches your code. That way you add the token in one place instead of in a hundred components:

1// Request interceptor - adds authorization token
2spaceApi.interceptors.request.use(config => {
3  const token = localStorage.getItem('authToken');
4  if (token) {
5    config.headers.Authorization = `Bearer ${token}`;
6  }
7  console.log('Sending request:', config.url);
8  return config;
9});

A request interceptor must return config, otherwise the request never departs. The second filter works on the way back: it takes one function for success and one for errors, so you can react globally to an expired session (401) or too many requests (429):

1// Response interceptor - error handling
2spaceApi.interceptors.response.use(
3  response => {
4    // Success - return data
5    return response;
6  },
7  error => {
8    // Error - handle globally
9    if (error.response?.status === 401) {
10      console.log('Session expired - redirecting to login');
11      window.location.href = '/login';
12    }
13    if (error.response?.status === 429) {
14      console.log('Too many requests - try again later');
15    }
16    return Promise.reject(error);
17  }
18);

The Promise.reject(error) at the end matters: without it the error would be "swallowed" and the component would think everything went fine.

Request and Response Transformation

The transformRequest and transformResponse options let you reshape data right before sending and right after receiving. Here we add a timestamp to every request and a fetchedAt field to every response:

1const api = axios.create({
2  baseURL: 'https://api.space-center.com',
3  // Transform before sending
4  transformRequest: [(data) => {
5    // Add timestamp to every request
6    return JSON.stringify({
7      ...data,
8      timestamp: Date.now(),
9      source: 'space-station',
10    });
11  }],
12  // Transform after receiving
13  transformResponse: [(data) => {
14    const parsed = JSON.parse(data);
15    // Add a "fetchedAt" field to every response
16    return { ...parsed, fetchedAt: new Date().toISOString() };
17  }],
18});

Watch out: your own array replaces Axios's default transforms, which normally turn an object into JSON and parse the response. That's why we call JSON.stringify and JSON.parse ourselves here. For simple additions, interceptors are often more convenient because they don't switch off any defaults.

Error Handling in Axios

Since Axios throws errors for 4xx and 5xx codes, the error object tells you at which stage things broke. error.response exists when the server replied, error.request when the request went out but no response came back:

1async function fetchPlanet(id) {
2  try {
3    const { data } = await spaceApi.get(
4      `/planets/${id}`
5    );
6    return data;
7  } catch (error) {
8    if (error.response) {
9      // Server responded with error code (4xx, 5xx)
10      console.error('Status:', error.response.status);
11      console.error('Data:', error.response.data);
12    } else if (error.request) {
13      // Request sent, but no response (timeout, network)
14      console.error('No response from server');
15    } else {
16      // Error in request configuration
17      console.error('Configuration error:', error.message);
18    }
19    throw error;
20  }
21}

So a 404 status lives in error.response.status, not in error.message. At the end we rethrow the error so the component can show a message.

Axios in a React Component

In a component we put it all together: the instance, loading and error states, and cancelling the request when the component leaves the screen. In older code you'll see CancelToken used for that:

1// DEPRECATED (since Axios 0.22) - you'll meet this in older projects
2const source = axios.CancelToken.source();
3spaceApi.get('/people/', { cancelToken: source.token });
4source.cancel('Component unmounted');

The Axios docs mark CancelToken as deprecated, so in new code we use the standard AbortController and the signal option, the same one fetch() uses:

1import React, { useState, useEffect } from 'react';
2import axios from 'axios';
3
4const spaceApi = axios.create({
5  baseURL: 'https://swapi.dev/api',
6  timeout: 8000,
7});
8
9function CrewList() {
10  const [crew, setCrew] = useState([]);
11  const [loading, setLoading] = useState(true);
12  const [error, setError] = useState(null);
13
14  useEffect(() => {
15    const controller = new AbortController();
16
17    async function loadCrew() {
18      try {
19        const { data } = await spaceApi.get('/people/', {
20          signal: controller.signal,
21        });
22        setCrew(data.results);
23      } catch (err) {
24        if (!axios.isCancel(err)) {
25          setError(err.message);
26        }
27      } finally {
28        setLoading(false);
29      }
30    }
31
32    loadCrew();
33    return () => controller.abort();
34  }, []);
35
36  if (loading) return <p>Scanning crew...</p>;
37  if (error) return <p>Error: {error}</p>;
38
39  return (
40    <ul>
41      {crew.map(member => (
42        <li key={member.name}>{member.name}</li>
43      ))}
44    </ul>
45  );
46}

axios.isCancel(err) recognizes a cancellation (Axios reports it as a CanceledError), so we don't show it as an error. The rest of the component hasn't changed: the states and conditional rendering look exactly as they do with fetch(). We'll look at them in detail in the next lesson, and at cancellation in the lesson on race conditions.

Remember: Axios is the bridge's radio station, one configuration, shared filters and clear errors for the whole fleet.

Code for this lesson: App.jsx
1import React, { useState, useEffect } from 'react';
2
3// Simulation of the Axios API (in a real project: import axios from 'axios')
4const axios = {
5  create: (config) => {
6    const instance = {
7      defaults: config,
8      interceptors: {
9        request: { handlers: [], use: (fn) => instance.interceptors.request.handlers.push(fn) },
10        response: { handlers: [], use: (ok, err) => instance.interceptors.response.handlers.push({ ok, err }) },
11      },
12      get: async (path) => {
13        const url = config.baseURL + path;
14        // Run request interceptors
15        let reqConfig = { url, headers: { ...config.headers } };
16        for (const handler of instance.interceptors.request.handlers) {
17          reqConfig = handler(reqConfig);
18        }
19        console.log('[Axios Request]', reqConfig.url, reqConfig.headers);
20
21        const response = await fetch(url, {
22          headers: reqConfig.headers,
23          signal: reqConfig.signal,
24        });
25        if (!response.ok) {
26          const error = new Error(`HTTP ${response.status}`);
27          error.response = { status: response.status, data: null };
28          // Run error interceptors
29          for (const handler of instance.interceptors.response.handlers) {
30            if (handler.err) handler.err(error);
31          }
32          throw error;
33        }
34        const data = await response.json();
35        const result = { data, status: response.status, headers: response.headers };
36        // Run success interceptors
37        for (const handler of instance.interceptors.response.handlers) {
38          if (handler.ok) handler.ok(result);
39        }
40        return result;
41      },
42    };
43    return instance;
44  },
45};
46
47// Create an API instance
48const spaceApi = axios.create({
49  baseURL: 'https://swapi.dev/api',
50  timeout: 10000,
51  headers: { 'X-Station-Id': 'ISS-2150' },
52});
53
54// Request interceptor - logging
55spaceApi.interceptors.request.use(config => {
56  console.log('[Interceptor] Adding timestamp to request');
57  config.headers['X-Request-Time'] = new Date().toISOString();
58  return config;
59});
60
61// Response interceptor
62spaceApi.interceptors.response.use(
63  response => { console.log('[Interceptor] Success:', response.status); },
64  error => { console.log('[Interceptor] Error:', error.response?.status); }
65);
66
67function AxiosDemo() {
68  const [planets, setPlanets] = useState([]);
69  const [loading, setLoading] = useState(true);
70  const [error, setError] = useState(null);
71  const [logs, setLogs] = useState([]);
72
73  useEffect(() => {
74    async function loadPlanets() {
75      try {
76        setLogs(prev => [...prev, 'Sending request via Axios...']);
77        const { data } = await spaceApi.get('/planets/');
78        setPlanets(data.results);
79        setLogs(prev => [...prev, 'Data fetched! (automatic JSON)']);
80      } catch (err) {
81        setError(err.message);
82        setLogs(prev => [...prev, 'Axios threw an error: ' + err.message]);
83      } finally {
84        setLoading(false);
85      }
86    }
87    loadPlanets();
88  }, []);
89
90  return (
91    <div className="app">
92      <h1>Axios - HTTP Library</h1>
93      <div className="logs">
94        <h3>Interceptor logs:</h3>
95        {logs.map((log, i) => <p key={i} className="log">{log}</p>)}
96      </div>
97      {loading && <div className="loading"><div className="spinner"></div></div>}
98      {error && <div className="error"><p>{error}</p></div>}
99      {!loading && !error && (
100        <div className="planet-list">
101          {planets.map(planet => (
102            <div key={planet.name} className="planet-card">
103              <h3>{planet.name}</h3>
104              <p><strong>Climate:</strong> {planet.climate}</p>
105              <p><strong>Population:</strong> {planet.population}</p>
106            </div>
107          ))}
108        </div>
109      )}
110    </div>
111  );
112}
113
114export default AxiosDemo;

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What is the key difference between Axios and the built-in fetch() in handling HTTP errors?

  2. 2. What are interceptors used for in Axios?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Fetching data with Axios

  • Click in order

    Click the elements in order to create a configured Axios instance:

  • Vertical ordering

    Arrange the steps for adding a request interceptor in Axios:

Useful articles