From Zero to AI

Lesson 4.5: Axios - Alternative to Fetch

Duration: 60 minutes

Learning Objectives

By the end of this lesson, you will be able to:

  1. Install and configure Axios in a TypeScript project
  2. Make HTTP requests using Axios
  3. Understand the benefits of Axios over native Fetch
  4. Configure Axios instances with defaults
  5. Use interceptors for request/response modification
  6. Handle errors with Axios

Introduction

Axios is a popular HTTP client library that provides a cleaner API and additional features compared to the native Fetch API. While Fetch is built into browsers, Axios offers conveniences that make working with APIs easier.

// Fetch
const response = await fetch("https://api.example.com/users");
if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
const users = await response.json();

// Axios
const { data: users } = await axios.get("https://api.example.com/users");

Installation

# Install Axios
npm install axios

# TypeScript types are included in axios package
import axios from 'axios';

Why Axios?

Feature Fetch Axios
Built-in Yes No (npm install)
Automatic JSON parsing No Yes
Throws on HTTP errors No Yes
Request timeout Manual Built-in
Request cancellation Manual Built-in
Interceptors No Yes
Progress tracking No Yes
Browser support Modern All (includes polyfills)
Node.js support v18+ All versions

Basic Usage

GET Requests

import axios from "axios";

// Simple GET
const response = await axios.get("https://api.example.com/users");
console.log(response.data); // Already parsed JSON

// GET with query parameters
const response = await axios.get("https://api.example.com/users", {
  params: {
    role: "admin",
    limit: 10
  }
});
// Equivalent to: /users?role=admin&limit=10

POST Requests

// POST with JSON body (automatic serialization)
const response = await axios.post('https://api.example.com/users', {
  name: 'John Doe',
  email: 'john@example.com',
});

console.log(response.data); // Created user
console.log(response.status); // 201

PUT and PATCH Requests

// PUT - Replace resource
await axios.put('https://api.example.com/users/1', {
  name: 'John Smith',
  email: 'johnsmith@example.com',
});

// PATCH - Partial update
await axios.patch('https://api.example.com/users/1', {
  email: 'newemail@example.com',
});

DELETE Requests

await axios.delete('https://api.example.com/users/1');

The Axios Response Object

const response = await axios.get('https://api.example.com/users');

// Response structure
console.log(response.data); // Response body (parsed)
console.log(response.status); // HTTP status code (200)
console.log(response.statusText); // Status text ("OK")
console.log(response.headers); // Response headers
console.log(response.config); // Request configuration

Request Configuration

const response = await axios({
  method: 'post',
  url: 'https://api.example.com/users',
  headers: {
    'Content-Type': 'application/json',
    Authorization: 'Bearer token123',
  },
  data: {
    name: 'John',
    email: 'john@example.com',
  },
  params: {
    notify: true,
  },
  timeout: 5000, // 5 seconds
  responseType: 'json',
});

Common Config Options

Option Description
method HTTP method
url Request URL
baseURL Prepended to url
headers Request headers
params URL query parameters
data Request body
timeout Request timeout in ms
responseType Response type (json, text, blob)
signal AbortController signal

Creating an Axios Instance

For real projects, create a configured instance:

import axios, { AxiosInstance } from 'axios';

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

// Now all requests use this config
const users = await api.get('/users'); // https://api.example.com/users
const post = await api.post('/posts', { title: 'Hello' });

Multiple Instances for Different APIs

// Main API
const mainApi = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
});

// Payment API with different config
const paymentApi = axios.create({
  baseURL: 'https://payments.example.com',
  timeout: 30000, // Longer timeout for payments
  headers: {
    'X-API-Version': '2',
  },
});

// Usage
const users = await mainApi.get('/users');
const payment = await paymentApi.post('/charge', { amount: 100 });

TypeScript with Axios

Typing Responses

interface User {
  id: number;
  name: string;
  email: string;
}

// Method 1: Generic type parameter
const response = await axios.get<User[]>("https://api.example.com/users");
const users: User[] = response.data;

// Method 2: Type the destructured data
const { data: users } = await axios.get<User[]>("https://api.example.com/users");

Typing Request Data

interface CreateUserRequest {
  name: string;
  email: string;
}

interface User {
  id: number;
  name: string;
  email: string;
}

const userData: CreateUserRequest = {
  name: 'John',
  email: 'john@example.com',
};

const { data: newUser } = await axios.post<User>('https://api.example.com/users', userData);

console.log(newUser.id); // TypeScript knows this is a number

Building a Typed API Client

import axios, { AxiosInstance } from 'axios';

interface User {
  id: number;
  name: string;
  email: string;
}

interface Post {
  id: number;
  userId: number;
  title: string;
  body: string;
}

interface CreateUserData {
  name: string;
  email: string;
}

interface CreatePostData {
  userId: number;
  title: string;
  body: string;
}

class ApiClient {
  private client: AxiosInstance;

  constructor(baseURL: string) {
    this.client = axios.create({
      baseURL,
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json',
      },
    });
  }

  // Users
  async getUsers(): Promise<User[]> {
    const { data } = await this.client.get<User[]>('/users');
    return data;
  }

  async getUser(id: number): Promise<User> {
    const { data } = await this.client.get<User>(`/users/${id}`);
    return data;
  }

  async createUser(userData: CreateUserData): Promise<User> {
    const { data } = await this.client.post<User>('/users', userData);
    return data;
  }

  async updateUser(id: number, updates: Partial<User>): Promise<User> {
    const { data } = await this.client.patch<User>(`/users/${id}`, updates);
    return data;
  }

  async deleteUser(id: number): Promise<void> {
    await this.client.delete(`/users/${id}`);
  }

  // Posts
  async getPosts(): Promise<Post[]> {
    const { data } = await this.client.get<Post[]>('/posts');
    return data;
  }

  async getUserPosts(userId: number): Promise<Post[]> {
    const { data } = await this.client.get<Post[]>(`/users/${userId}/posts`);
    return data;
  }

  async createPost(postData: CreatePostData): Promise<Post> {
    const { data } = await this.client.post<Post>('/posts', postData);
    return data;
  }
}

// Usage
const api = new ApiClient('https://jsonplaceholder.typicode.com');

const users = await api.getUsers();
const user = await api.getUser(1);
const posts = await api.getUserPosts(1);

Error Handling

Axios Throws on HTTP Errors

Unlike Fetch, Axios automatically throws for 4xx and 5xx status codes:

try {
  const response = await axios.get('https://api.example.com/not-found');
} catch (error) {
  // This WILL be executed for 404
  console.log('Request failed');
}

The AxiosError Object

import axios, { AxiosError, isAxiosError } from 'axios';

try {
  await axios.get('https://api.example.com/users/999');
} catch (error) {
  if (isAxiosError(error)) {
    // Type-safe access to error properties
    console.log('Status:', error.response?.status); // 404
    console.log('Data:', error.response?.data); // Error body from server
    console.log('Headers:', error.response?.headers);
    console.log('Request:', error.config); // Original request config

    if (error.code === 'ECONNABORTED') {
      console.log('Request timed out');
    }
  }
}

Comprehensive Error Handling

import axios, { AxiosError, isAxiosError } from 'axios';

interface ApiErrorResponse {
  message: string;
  code: string;
}

async function fetchUserSafe(id: number): Promise<User | null> {
  try {
    const { data } = await axios.get<User>(`https://api.example.com/users/${id}`);
    return data;
  } catch (error) {
    if (!isAxiosError(error)) {
      throw error; // Re-throw non-Axios errors
    }

    const axiosError = error as AxiosError<ApiErrorResponse>;

    // No response (network error, timeout)
    if (!axiosError.response) {
      if (axiosError.code === 'ECONNABORTED') {
        throw new Error('Request timed out');
      }
      throw new Error('Network error: Unable to reach server');
    }

    // Handle specific status codes
    switch (axiosError.response.status) {
      case 404:
        return null; // User not found
      case 401:
        throw new Error('Authentication required');
      case 403:
        throw new Error('Access denied');
      case 500:
        throw new Error('Server error. Please try again later.');
      default:
        throw new Error(axiosError.response.data?.message || 'An error occurred');
    }
  }
}

Interceptors

Interceptors let you transform requests before they are sent and responses before they are handled.

Request Interceptors

const api = axios.create({
  baseURL: 'https://api.example.com',
});

// Add auth token to every request
api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('authToken');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// Log all requests
api.interceptors.request.use((config) => {
  console.log(`Making ${config.method?.toUpperCase()} request to ${config.url}`);
  return config;
});

Response Interceptors

// Handle token refresh on 401
api.interceptors.response.use(
  (response) => response, // Pass through successful responses
  async (error) => {
    if (error.response?.status === 401) {
      // Try to refresh token
      try {
        const newToken = await refreshAuthToken();
        localStorage.setItem('authToken', newToken);

        // Retry original request with new token
        error.config.headers.Authorization = `Bearer ${newToken}`;
        return api.request(error.config);
      } catch {
        // Refresh failed, redirect to login
        window.location.href = '/login';
      }
    }
    return Promise.reject(error);
  }
);

// Transform response data
api.interceptors.response.use((response) => {
  // Unwrap data automatically
  return response.data;
});

Logging Interceptor

api.interceptors.request.use((config) => {
  config.metadata = { startTime: new Date() };
  return config;
});

api.interceptors.response.use(
  (response) => {
    const duration = new Date().getTime() - response.config.metadata.startTime.getTime();
    console.log(`${response.config.method?.toUpperCase()} ${response.config.url} - ${duration}ms`);
    return response;
  },
  (error) => {
    const duration = new Date().getTime() - error.config.metadata.startTime.getTime();
    console.log(
      `${error.config.method?.toUpperCase()} ${error.config.url} - FAILED - ${duration}ms`
    );
    return Promise.reject(error);
  }
);

Request Cancellation

// Using AbortController (recommended)
const controller = new AbortController();

const request = axios.get('https://api.example.com/large-data', {
  signal: controller.signal,
});

// Cancel after 5 seconds
setTimeout(() => {
  controller.abort();
}, 5000);

try {
  const response = await request;
} catch (error) {
  if (axios.isCancel(error)) {
    console.log('Request was cancelled');
  }
}

Cancel on Component Unmount (React pattern)

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

  const fetchData = async () => {
    try {
      const { data } = await axios.get('/api/data', {
        signal: controller.signal,
      });
      setData(data);
    } catch (error) {
      if (!axios.isCancel(error)) {
        setError(error);
      }
    }
  };

  fetchData();

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

Axios vs Fetch Comparison

Creating a POST Request

// Fetch
const response = await fetch("https://api.example.com/users", {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify({ name: "John" })
});
if (!response.ok) {
  throw new Error(`HTTP error: ${response.status}`);
}
const user = await response.json();

// Axios
const { data: user } = await axios.post("https://api.example.com/users", {
  name: "John"
});

Error Handling

// Fetch - Need to check response.ok manually
try {
  const response = await fetch('/api/users/999');
  if (!response.ok) {
    // Handle HTTP error manually
    throw new Error(`HTTP ${response.status}`);
  }
  const data = await response.json();
} catch (error) {
  // Could be network error or our thrown error
}

// Axios - Automatically throws on HTTP errors
try {
  const { data } = await axios.get('/api/users/999');
} catch (error) {
  if (isAxiosError(error)) {
    console.log(error.response?.status); // 404
    console.log(error.response?.data); // Error response body
  }
}

Query Parameters

// Fetch - Manual URL building
const params = new URLSearchParams({ page: '1', limit: '10' });
const response = await fetch(`https://api.example.com/users?${params}`);

// Axios - Built-in params option
const { data } = await axios.get('https://api.example.com/users', {
  params: { page: 1, limit: 10 },
});

Exercises

Exercise 1: Basic Axios Requests

Use Axios to fetch all posts from JSONPlaceholder and display titles of posts by user 1:

async function getPostsByUser(userId: number): Promise<void> {}

getPostsByUser(1);
Solution
import axios from 'axios';

interface Post {
  id: number;
  userId: number;
  title: string;
  body: string;
}

async function getPostsByUser(userId: number): Promise<void> {
  const { data: posts } = await axios.get<Post[]>('https://jsonplaceholder.typicode.com/posts', {
    params: { userId },
  });

  console.log(`Posts by user ${userId}:`);
  posts.forEach((post, index) => {
    console.log(`${index + 1}. ${post.title}`);
  });
}

getPostsByUser(1);

Exercise 2: Create an API Client

Create a reusable API client with authentication:

class TodoApi {
  // Implement:
  // - Constructor that accepts a token
  // - getTodos(): Promise<Todo[]>
  // - createTodo(title: string): Promise<Todo>
  // - completeTodo(id: number): Promise<Todo>
  // - deleteTodo(id: number): Promise<void>
}
Solution
import axios, { AxiosInstance } from 'axios';

interface Todo {
  id: number;
  userId: number;
  title: string;
  completed: boolean;
}

class TodoApi {
  private client: AxiosInstance;

  constructor(token: string) {
    this.client = axios.create({
      baseURL: 'https://jsonplaceholder.typicode.com',
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`,
      },
    });
  }

  async getTodos(): Promise<Todo[]> {
    const { data } = await this.client.get<Todo[]>('/todos');
    return data;
  }

  async createTodo(title: string): Promise<Todo> {
    const { data } = await this.client.post<Todo>('/todos', {
      title,
      completed: false,
      userId: 1,
    });
    return data;
  }

  async completeTodo(id: number): Promise<Todo> {
    const { data } = await this.client.patch<Todo>(`/todos/${id}`, {
      completed: true,
    });
    return data;
  }

  async deleteTodo(id: number): Promise<void> {
    await this.client.delete(`/todos/${id}`);
  }
}

// Usage
const api = new TodoApi('my-auth-token');
const todos = await api.getTodos();
const newTodo = await api.createTodo('Learn Axios');
await api.completeTodo(newTodo.id);

Exercise 3: Add Interceptors

Add interceptors to the API client that:

  1. Log all requests
  2. Add a timestamp to requests
  3. Handle 401 errors by showing an alert
Solution
import axios, { AxiosInstance } from 'axios';

function createApiClient(baseURL: string): AxiosInstance {
  const client = axios.create({
    baseURL,
    timeout: 10000,
  });

  // Request interceptor - log and add timestamp
  client.interceptors.request.use((config) => {
    console.log(`[${new Date().toISOString()}] ${config.method?.toUpperCase()} ${config.url}`);
    config.headers['X-Request-Time'] = new Date().toISOString();
    return config;
  });

  // Response interceptor - handle 401
  client.interceptors.response.use(
    (response) => response,
    (error) => {
      if (error.response?.status === 401) {
        alert('Your session has expired. Please log in again.');
        // In a real app: redirect to login page
      }
      return Promise.reject(error);
    }
  );

  return client;
}

const api = createApiClient('https://api.example.com');

When to Use Axios vs Fetch

Use Fetch when:

  • You want zero dependencies
  • You are making simple requests
  • You are targeting modern browsers/Node.js only
  • Bundle size is critical

Use Axios when:

  • You need interceptors for auth/logging
  • You want automatic error handling for HTTP errors
  • You need better TypeScript support out of the box
  • You want cleaner syntax for complex requests
  • You need to support older browsers

Key Takeaways

  1. Axios provides a cleaner API than Fetch with less boilerplate
  2. Automatic JSON parsing - no need to call .json()
  3. Automatic error throwing for HTTP errors (4xx, 5xx)
  4. Instances let you configure reusable API clients
  5. Interceptors enable request/response transformation
  6. Use isAxiosError() for type-safe error handling
  7. TypeScript generics provide excellent type inference
  8. AbortController works with Axios for cancellation

Resources

Resource Type Level
Axios Documentation Documentation Beginner
Axios GitHub Repository Beginner
Axios vs Fetch Article Intermediate
Axios Interceptors Documentation Intermediate

Next Lesson

Now that you know both Fetch and Axios, let us put everything together and build a complete application using JSONPlaceholder API.

Continue to Lesson 4.6: Practice - JSONPlaceholder API