Endurative

Fetch vs Axios in Scalable JavaScript Architectures

In modern frontend development, making API calling scalable is a core part of almost every application. Two primary tools are commonly used in JavaScript for this purpose: the native fetch API and the third-party library Axios.

While both are capable of handling API requests, they offer very different experiences when used in larger or scalable applications. This guide walks through how each works and what to consider when building scalable JavaScript architectures.

1. Basic Usage

Using fetch requires some manual steps. Here's how a POST request with JSON typically looks:

fetch("/api/user", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "John" }),
})
.then((res) => {
if (!res.ok) throw new Error("Request failed");
return res.json();
})
.then((data) => console.log(data))
.catch((err) => console.error(err));

In contrast, Axios simplifies it:

axios.post("/api/user", { name: "John" })
.then((res) => console.log(res.data))
.catch((err) => console.error(err));

Axios automatically stringifies the body and parses the response JSON, which reduces boilerplate.

2. Global Configuration & Reuse

In scalable apps, you'll often need to make requests with common configurations like a base URL, timeout, headers, and auth tokens.

With fetch, this requires building a custom wrapper function:

function customFetch(path, options = {}) {
const token = localStorage.getItem("token");
return fetch(`https://api.example.com${path}`, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
}).then((res) => res.json());
}

Axios provides a much cleaner way with instances:

const authAxios = axios.create({
baseURL: "https://api.example.com",
timeout: 5000,
headers: {
"Content-Type": "application/json",
},
});

Once created, this instance can be used throughout your app without repeating configuration:

authAxios.get('/profile');
authAxios.post('/update', { name: 'Alice' });

3. Adding Tokens & Interceptors

A scalable app often needs to inject authentication tokens, handle errors globally, or modify headers on every request.

With fetch, this logic must be handled manually for every request or abstracted in a custom wrapper.

Axios, however, supports interceptors out of the box:

authAxios.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});

This allows you to manage all request modifications in a single place, making it easy to scale and maintain.

4. Handling Query Params

When working with filters, pagination, or multi-value parameters, query string formatting becomes messy.

With fetch, you'd need to write your own serializer:

function toQueryString(params) {
return Object.entries(params)
.map(([key, value]) =>
Array.isArray(value)
? value.map((v) => `${key}[]=${v}`).join("&")
: `${key}=${value}`
)
.join("&");
}

fetch(`/api/items?${toQueryString({ tags: ["js", "dev"], limit: 10 })}`);

Axios handles this internally and cleanly:

axios.get('/api/items', {
params: { tags: ['js', 'dev'], limit: 10 },
});

It automatically serializes arrays and nested objects into proper query strings.

5. Error Handling

One major difference is how they treat HTTP errors. fetch does not reject the promise for HTTP errors like 404 or 500:

fetch("/api/invalid").then((res) => {
if (!res.ok) {
throw new Error("Request failed");
}
});

Axios does reject the promise for non-2xx status codes, making error handling more straightforward:

axios.get("/api/invalid").catch((err) => {
console.log(err.response.status); // 404
});

Real-World Axios Setup

Here’s how a typical scalable Axios setup might look:

// src/utils/axios.js
import axios from "axios";
import Qs from "qs";

const authAxios = axios.create({
baseURL: "https://api.example.com",
paramsSerializer: (params) =>
Qs.stringify(params, {
arrayFormat: "brackets",
}),
});

authAxios.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});

export default authAxios;

And usage:

import authAxios from 'src/utils/axios';

authAxios.get('/user/profile');
authAxios.post('/user/update', { name: 'New Name' });

Final Thoughts: Which One Should You Use?

  1. If you're building a small app or want zero dependencies, fetch can work well — though you’ll likely need to build wrappers for reuse, token injection, error handling, and query params.
  2. For scalable applications, Axios is a better choice. Its built-in features — like interceptors, instances, JSON handling, and serialization — make it easy to manage and grow over time without rewriting logic.
Both tools are powerful, but Axios offers a smoother developer experience when building for scale.