Why you need a layer above fetch

In a small demo fetch('/api/users') is enough. In a real app every request repeats the same chores: base URL, the Authorization header, the response.ok check, JSON parsing, timeout, retries, one error format. If all of this is copied into 40 files, changing the token format means editing 40 places.

The fix is a single HTTP client that every request goes through. There are two ways: a ready library (Axios, ky, ofetch) or your own 30-50 line wrapper over fetch.

The HTTP client pipeline: request hook, network, response hook
The HTTP client pipeline: request hook, network, response hook

💡 The code in this lesson runs without a network: instead of fetch we pass a fake function (fetchImpl). The same trick helps in tests.


fetch vs Axios

fetch ships in browsers and Node 18+, no extra package needed. It has two well-known quirks: the promise does not reject on 404 and 500, and there is no timeout. Axios fills those gaps but adds code to the bundle.

javascript
const res = await fetch('/api/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Aysel' }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const user = await res.json();

// Axios
import axios from 'axios';

const { data: user } = await axios.post('/api/users', {
  name: 'Aysel',
});
// non-2xx statuses reject the promise automatically
TopicfetchAxios
4xx/5xxno rejection, check okrejects (configurable via validateStatus)
JSONmanualautomatic, response.data
TimeoutAbortControllertimeout option
Interceptorsnonebuilt in
Upload progressnoonUploadProgress

💡 Rule of thumb: in a new small project use fetch plus your own wrapper; in a large app with many endpoints and interceptors use Axios or ky.


Axios instance and interceptors

axios.create() returns an independent instance with shared configuration. Interceptors are functions that run before a request is sent and after a response arrives.

javascript
import axios from 'axios';

export const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 5000,
  headers: { Accept: 'application/json' },
});

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