React · Routing & Data

Fetch and Axios

You can talk to APIs with the built-in fetch or the popular axios library. Both work; what matters is wrapping them in one small API module so components stay simple.

Intermediate 6 min Lesson 23 of 42Examples target React 19 with Vite.
Before you start:API Integration
0/42 in React
On this page · 6 min · Intermediate

Learning objectives#

  • Send GET, POST, PUT and DELETE with fetch
  • Do the same with axios
  • Compare their error handling and defaults
  • Build a reusable API client with a base URL and auth header

Explanation#

fetch is built into browsers. For JSON you set Content-Type and JSON.stringify the body, call res.json() yourself, and check res.ok.

axios (npm install axios) parses JSON automatically, throws for any non-2xx status, supports interceptors (add a token to every request, handle 401 globally) and has a simple timeout option.

Either way, do not scatter fetch calls through components. Create an api.js module with functions like getProducts() and createProduct(data).

Configure the base URL from an environment variable: import.meta.env.VITE_API_URL in Vite.

Send auth tokens in the Authorization header; with cookies, use credentials: "include" (fetch) or withCredentials: true (axios).

Syntax#

JavaScript Illustrative
// fetch
const res = await fetch('/api/products', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(product),
});
if (!res.ok) throw new Error(res.status);
const created = await res.json();


// axios
const { data: created } = await axios.post('/api/products', product);

Code example: One API module, two implementations#

JavaScriptsrc/api.js Illustrative
import axios from 'axios';


const BASE_URL = import.meta.env.VITE_API_URL ?? 'https://jsonplaceholder.typicode.com';


// ── fetch version ──────────────────────────────────────────
async function request(path, { method = 'GET', body, token } = {}) {
  const res = await fetch(BASE_URL + path, {
    method,
    headers: {
      ...(body ? { 'Content-Type': 'application/json' } : {}),
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  if (!res.ok) throw new Error(`${method} ${path} failed: ${res.status}`);
  return res.status === 204 ? null : res.json();
}


export const fetchApi = {
  listTodos: () => request('/todos?_limit=5'),
  createTodo: todo => request('/todos', { method: 'POST', body: todo }),
  updateTodo: (id, todo) => request(`/todos/${id}`, { method: 'PUT', body: todo }),
  deleteTodo: id => request(`/todos/${id}`, { method: 'DELETE' }),
};


// ── axios version ──────────────────────────────────────────
const http = axios.create({ baseURL: BASE_URL, timeout: 10000 });


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


export const axiosApi = {
  listTodos: () => http.get('/todos', { params: { _limit: 5 } }).then(r => r.data),
  createTodo: todo => http.post('/todos', todo).then(r => r.data),
  updateTodo: (id, todo) => http.put(`/todos/${id}`, todo).then(r => r.data),
  deleteTodo: id => http.delete(`/todos/${id}`),
};


// In a component:
//   const todos = await fetchApi.listTodos();
//   await axiosApi.createTodo({ title: 'Learn axios', completed: false });
Expected output
Both objects expose the same four functions. fetchApi.listTodos() and axiosApi.listTodos() each resolve to an array of 5 todos. createTodo resolves to the created todo (the test API returns it with id 201). A 404 throws in both versions — fetchApi via the res.ok check, axios automatically.

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. BASE_URL comes from VITE_API_URL so dev, staging and production can differ.
  2. The fetch helper adds JSON headers and the token only when needed, checks res.ok and handles 204 No Content.
  3. axios.create makes an instance with a base URL and timeout; an interceptor adds the token to every request.
  4. Components import api functions and never build URLs or headers themselves.

Real-world use case#

A React admin app keeps every endpoint in src/api/*.js. When the backend moves from /api/v1 to /api/v2, one file changes and no component is touched.

Common mistakes#

Forgetting Content-Type with fetch

Without application/json, many servers (ASP.NET Core [FromBody], Express json()) see an empty or unparsed body.

Expecting fetch to throw on 500

It does not. Check res.ok. axios does throw.

Hard-coding localhost URLs

The production build will still call localhost. Use environment variables.

Best practices#

  • One API module per backend area
  • Base URL from env variables
  • Central auth header and error handling
  • Return plain data to components

Remember this#

fetch: built-in, check res.ok, JSON.stringify yourself.

axios: auto JSON, throws on errors, interceptors.

Components call api.getX(), never raw URLs.

Practice exercise#

Your task: Add a getTodo(id) function to fetchApi and a component that loads todo 1 and shows its title.

Need a hint?
getTodo: id => request(`/todos/${id}`). Use it in an effect.
Show solution
JSX Runnable
import { useEffect, useState } from 'react';


const getTodo = async id => {
  const res = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
  if (!res.ok) throw new Error(res.status);
  return res.json();
};


export default function TodoTitle() {
  const [title, setTitle] = useState('Loading…');
  useEffect(() => {
    getTodo(1).then(t => setTitle(t.title)).catch(() => setTitle('Failed to load'));
  }, []);
  return <p>{title}</p>;
}

Mini quiz#

Fetch and Axios quizMultiple choice Question 1 of 3

Which statement is true?

Score so far: 0/0

Want more? Practise all React MCQs · React interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.