Axios - Promise-based HTTP Client

Learn how to use Axios for making HTTP requests in React and Next.js applications with automatic JSON parsing, interceptors, error handling, and request cancellation.

Axios - Promise-based HTTP Client

Axios

Axios is a promise-based HTTP client for both the browser and Node.js. It provides a clean, intuitive API for making HTTP requests with features like automatic JSON parsing, request/response interceptors, and built-in error handling.

What is Axios?

Axios is a popular alternative to the native fetch API that simplifies HTTP requests and provides additional features:

  • Promise-based API: Works with async/await and promises
  • Automatic JSON transformation: No need to manually parse responses
  • Request/Response interceptors: Transform requests and responses globally
  • Request cancellation: Cancel pending requests when needed
  • Progress tracking: Monitor upload/download progress
  • Browser and Node.js support: Isomorphic - works everywhere
  • CSRF protection: Built-in XSRF token support

Installation

Install Axios in your Next.js project:

yarn add axios

Basic Usage

Simple GET Request

import axios from 'axios';

// Basic GET request
const response = await axios.get('https://api.example.com/users');
console.log(response.data); // Automatically parsed JSON

// GET with query parameters
const response = await axios.get('https://api.example.com/users', {
  params: {
    page: 1,
    limit: 10,
    sort: 'name'
  }
});

POST Request

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

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

PUT and PATCH Requests

// Update entire resource (PUT)
await axios.put('https://api.example.com/users/123', {
  name: 'Jane Doe',
  email: 'jane@example.com',
  age: 28
});

// Partial update (PATCH)
await axios.patch('https://api.example.com/users/123', {
  age: 29 // Only update age
});

DELETE Request

// Delete a resource
await axios.delete('https://api.example.com/users/123');

Axios vs Fetch

Here's why developers prefer Axios over native fetch:

Fetch API

// Fetch - more boilerplate
const response = await fetch('https://api.example.com/users');

if (!response.ok) {
  throw new Error('Request failed');
}

const data = await response.json(); // Manual parsing

// POST with fetch - verbose
const response = await fetch('https://api.example.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ name: 'John' })
});

Axios

// Axios - cleaner and simpler
const { data } = await axios.get('https://api.example.com/users');
// data is already parsed, errors throw automatically

// POST with axios - cleaner
const { data } = await axios.post('https://api.example.com/users', {
  name: 'John'
});
// Headers and JSON.stringify handled automatically

Using Axios in Next.js Components

Client Component Example

'use client';

import { useState, useEffect } from 'react';
import axios from 'axios';

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

export default function UsersPage() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        const { data } = await axios.get<User[]>('/api/users');
        setUsers(data);
      } catch (err) {
        setError('Failed to load users');
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div>
      <h1>Users</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}

Creating a User

'use client';

import { useState } from 'react';
import axios from 'axios';

export default function CreateUser() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);

    try {
      const { data } = await axios.post('/api/users', {
        name,
        email
      });

      alert(`User created: ${data.name}`);
      setName('');
      setEmail('');
    } catch (error) {
      if (axios.isAxiosError(error)) {
        alert(`Error: ${error.response?.data?.message || error.message}`);
      }
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
        placeholder="Name"
        required
      />
      <input
        value={email}
        onChange={e => setEmail(e.target.value)}
        placeholder="Email"
        type="email"
        required
      />
      <button disabled={loading}>
        {loading ? 'Creating...' : 'Create User'}
      </button>
    </form>
  );
}

Creating an Axios Instance

Create a configured instance for reusability:

// lib/axios.ts
import axios from 'axios';

const apiClient = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api',
  timeout: 10000, // 10 seconds
  headers: {
    'Content-Type': 'application/json'
  }
});

export default apiClient;

Using the Instance

import apiClient from '@/lib/axios';

// All requests use the base configuration
const users = await apiClient.get('/users');
const user = await apiClient.post('/users', { name: 'John' });

Request Configuration

Axios supports extensive configuration options:

const response = await axios.request({
  method: 'POST',
  url: '/api/users',
  baseURL: 'https://api.example.com',
  headers: {
    'Authorization': 'Bearer token123',
    'Custom-Header': 'value'
  },
  params: {
    filter: 'active'
  },
  data: {
    name: 'John Doe',
    email: 'john@example.com'
  },
  timeout: 5000,
  withCredentials: true, // Send cookies with requests
  responseType: 'json', // json, text, blob, arraybuffer, stream
  validateStatus: (status) => status < 500 // Accept all < 500 as success
});

Interceptors

Interceptors allow you to run code before requests are sent or after responses are received.

Request Interceptors

// lib/axios.ts
import axios from 'axios';

const apiClient = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL
});

// Add authentication token to all requests
apiClient.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('auth_token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

export default apiClient;

Response Interceptors

// Handle errors globally
apiClient.interceptors.response.use(
  (response) => {
    // Transform response data if needed
    return response;
  },
  (error) => {
    if (error.response) {
      // Server responded with error status
      const status = error.response.status;

      if (status === 401) {
        // Redirect to login
        window.location.href = '/login';
      } else if (status === 403) {
        alert('You do not have permission to access this resource');
      } else if (status >= 500) {
        alert('Server error. Please try again later.');
      }
    } else if (error.request) {
      // Request made but no response received
      alert('Network error. Please check your connection.');
    }

    return Promise.reject(error);
  }
);

Complete Interceptor Setup

// lib/axios.ts
import axios from 'axios';

const apiClient = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  timeout: 10000
});

// Request interceptor - add auth token
apiClient.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('auth_token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    
    // Log all requests in development
    if (process.env.NODE_ENV === 'development') {
      console.log(`[${config.method?.toUpperCase()}] ${config.url}`);
    }
    
    return config;
  },
  (error) => Promise.reject(error)
);

// Response interceptor - handle errors globally
apiClient.interceptors.response.use(
  (response) => {
    // Log successful responses in development
    if (process.env.NODE_ENV === 'development') {
      console.log(`[${response.config.method?.toUpperCase()}] ${response.config.url} - ${response.status}`);
    }
    return response;
  },
  async (error) => {
    const originalRequest = error.config;

    // Handle 401 - try to refresh token
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        const { data } = await axios.post('/api/auth/refresh');
        localStorage.setItem('auth_token', data.token);
        
        // Retry original request with new token
        originalRequest.headers.Authorization = `Bearer ${data.token}`;
        return apiClient(originalRequest);
      } catch (refreshError) {
        // Refresh failed, redirect to login
        localStorage.removeItem('auth_token');
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }

    return Promise.reject(error);
  }
);

export default apiClient;

Error Handling

Axios provides detailed error information:

import axios from 'axios';

try {
  const { data } = await axios.get('/api/users');
} catch (error) {
  if (axios.isAxiosError(error)) {
    // Type-safe error handling
    if (error.response) {
      // Server responded with error status (4xx, 5xx)
      console.error('Status:', error.response.status);
      console.error('Data:', error.response.data);
      console.error('Headers:', error.response.headers);
    } else if (error.request) {
      // Request made but no response received
      console.error('No response received:', error.request);
    } else {
      // Error setting up request
      console.error('Request setup error:', error.message);
    }
  } else {
    // Non-Axios error
    console.error('Unexpected error:', error);
  }
}

Creating Custom Error Handler

// lib/errorHandler.ts
import axios, { AxiosError } from 'axios';

interface ErrorResponse {
  message: string;
  code?: string;
}

export function handleApiError(error: unknown): string {
  if (axios.isAxiosError(error)) {
    const axiosError = error as AxiosError<ErrorResponse>;
    
    if (axiosError.response) {
      // Extract error message from response
      return axiosError.response.data?.message || 'An error occurred';
    } else if (axiosError.request) {
      return 'Network error. Please check your connection.';
    }
  }
  
  return 'An unexpected error occurred';
}

// Usage
try {
  await axios.post('/api/users', userData);
} catch (error) {
  const errorMessage = handleApiError(error);
  alert(errorMessage);
}

Request Cancellation

Cancel requests when components unmount or when new requests are made:

'use client';

import { useEffect, useState } from 'react';
import axios from 'axios';

export default function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

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

    const search = async () => {
      if (!query) return;

      try {
        const { data } = await axios.get('/api/search', {
          params: { q: query },
          signal: controller.signal
        });
        setResults(data);
      } catch (error) {
        if (axios.isCancel(error)) {
          console.log('Request canceled:', error.message);
        } else {
          console.error('Search error:', error);
        }
      }
    };

    search();

    // Cancel request when component unmounts or query changes
    return () => controller.abort();
  }, [query]);

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <ul>
        {results.map((result: any) => (
          <li key={result.id}>{result.name}</li>
        ))}
      </ul>
    </div>
  );
}

Using CancelToken (Legacy)

import axios, { CancelTokenSource } from 'axios';

let cancelToken: CancelTokenSource | null = null;

async function searchProducts(query: string) {
  // Cancel previous request
  if (cancelToken) {
    cancelToken.cancel('New search initiated');
  }

  // Create new cancel token
  cancelToken = axios.CancelToken.source();

  try {
    const { data } = await axios.get('/api/products', {
      params: { q: query },
      cancelToken: cancelToken.token
    });
    return data;
  } catch (error) {
    if (axios.isCancel(error)) {
      console.log('Request canceled:', error.message);
    } else {
      throw error;
    }
  }
}

Progress Tracking

Monitor upload and download progress:

File Upload with Progress

'use client';

import { useState } from 'react';
import axios from 'axios';

export default function FileUpload() {
  const [progress, setProgress] = useState(0);
  const [uploading, setUploading] = useState(false);

  const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    const formData = new FormData();
    formData.append('file', file);

    setUploading(true);

    try {
      const { data } = await axios.post('/api/upload', formData, {
        headers: {
          'Content-Type': 'multipart/form-data'
        },
        onUploadProgress: (progressEvent) => {
          const percentCompleted = Math.round(
            (progressEvent.loaded * 100) / (progressEvent.total || 1)
          );
          setProgress(percentCompleted);
        }
      });

      alert('Upload complete!');
    } catch (error) {
      alert('Upload failed');
    } finally {
      setUploading(false);
      setProgress(0);
    }
  };

  return (
    <div>
      <input type="file" onChange={handleUpload} disabled={uploading} />
      {uploading && (
        <div>
          <div>Uploading: {progress}%</div>
          <progress value={progress} max="100" />
        </div>
      )}
    </div>
  );
}

Download with Progress

async function downloadFile(fileId: string) {
  try {
    const response = await axios.get(`/api/files/${fileId}/download`, {
      responseType: 'blob',
      onDownloadProgress: (progressEvent) => {
        const percentCompleted = Math.round(
          (progressEvent.loaded * 100) / (progressEvent.total || 1)
        );
        console.log(`Download progress: ${percentCompleted}%`);
      }
    });

    // Create download link
    const url = window.URL.createObjectURL(new Blob([response.data]));
    const link = document.createElement('a');
    link.href = url;
    link.setAttribute('download', 'file.pdf');
    document.body.appendChild(link);
    link.click();
    link.remove();
  } catch (error) {
    console.error('Download failed:', error);
  }
}

Working with Next.js API Routes

Creating API Routes

// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET() {
  try {
    // Fetch from external API or database
    const response = await fetch('https://api.example.com/users');
    const users = await response.json();

    return NextResponse.json(users);
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to fetch users' },
      { status: 500 }
    );
  }
}

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    
    // Validate data
    if (!body.name || !body.email) {
      return NextResponse.json(
        { error: 'Name and email are required' },
        { status: 400 }
      );
    }

    // Create user in database
    const user = await createUser(body);

    return NextResponse.json(user, { status: 201 });
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to create user' },
      { status: 500 }
    );
  }
}

Calling API Routes with Axios

import apiClient from '@/lib/axios';

// GET users
const { data: users } = await apiClient.get('/api/users');

// POST new user
const { data: newUser } = await apiClient.post('/api/users', {
  name: 'John Doe',
  email: 'john@example.com'
});

// PUT update user
await apiClient.put(`/api/users/${userId}`, {
  name: 'Jane Doe'
});

// DELETE user
await apiClient.delete(`/api/users/${userId}`);

Best Practices

1. Create Reusable API Functions

// lib/api/users.ts
import apiClient from '@/lib/axios';

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

export const usersApi = {
  getAll: () => apiClient.get<User[]>('/users'),
  
  getById: (id: string) => apiClient.get<User>(`/users/${id}`),
  
  create: (data: Omit<User, 'id'>) => 
    apiClient.post<User>('/users', data),
  
  update: (id: string, data: Partial<User>) =>
    apiClient.put<User>(`/users/${id}`, data),
  
  delete: (id: string) => apiClient.delete(`/users/${id}`)
};

// Usage
const { data: users } = await usersApi.getAll();
const { data: user } = await usersApi.create({ name: 'John', email: 'john@example.com' });

2. Type-Safe Requests

import axios, { AxiosResponse } from 'axios';

interface ApiResponse<T> {
  data: T;
  message: string;
}

async function fetchUser(id: string): Promise<User> {
  const response: AxiosResponse<ApiResponse<User>> = await axios.get(
    `/api/users/${id}`
  );
  return response.data.data;
}

3. Environment Variables

# .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
API_SECRET_KEY=your-secret-key
// lib/axios.ts
const apiClient = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  headers: {
    'X-API-Key': process.env.API_SECRET_KEY // Server-side only
  }
});

4. Retry Logic

import axios, { AxiosError } from 'axios';

async function fetchWithRetry<T>(
  url: string,
  maxRetries = 3
): Promise<T> {
  let lastError: AxiosError;

  for (let i = 0; i < maxRetries; i++) {
    try {
      const { data } = await axios.get<T>(url);
      return data;
    } catch (error) {
      lastError = error as AxiosError;
      
      // Don't retry client errors (4xx)
      if (lastError.response && lastError.response.status < 500) {
        throw lastError;
      }

      // Wait before retrying (exponential backoff)
      if (i < maxRetries - 1) {
        await new Promise(resolve => 
          setTimeout(resolve, Math.pow(2, i) * 1000)
        );
      }
    }
  }

  throw lastError!;
}

5. Request Timeout

const apiClient = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  timeout: 10000 // 10 seconds
});

// Or per request
await axios.get('/api/data', {
  timeout: 5000 // 5 seconds for this specific request
});

Common Patterns

Parallel Requests

// Fetch multiple resources in parallel
const [users, posts, comments] = await Promise.all([
  axios.get('/api/users'),
  axios.get('/api/posts'),
  axios.get('/api/comments')
]);

Sequential Requests

// Fetch user, then their posts
const { data: user } = await axios.get(`/api/users/${userId}`);
const { data: posts } = await axios.get(`/api/posts?userId=${user.id}`);

Conditional Requests

const { data: user } = await axios.get('/api/user');

if (user.isAdmin) {
  const { data: adminData } = await axios.get('/api/admin/dashboard');
}

Testing with Axios

Mocking Axios for Tests

import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';

const mock = new MockAdapter(axios);

// Mock GET request
mock.onGet('/api/users').reply(200, [
  { id: '1', name: 'John Doe', email: 'john@example.com' }
]);

// Mock POST request
mock.onPost('/api/users').reply((config) => {
  const data = JSON.parse(config.data);
  return [201, { id: '2', ...data }];
});

// Run your tests
test('fetches users', async () => {
  const { data } = await axios.get('/api/users');
  expect(data).toHaveLength(1);
  expect(data[0].name).toBe('John Doe');
});

Summary

Axios is a powerful, feature-rich HTTP client that simplifies API communication in Next.js applications. Key advantages include:

  • Clean API: Simpler syntax than native fetch
  • Automatic JSON handling: No manual parsing needed
  • Interceptors: Global request/response transformation
  • Error handling: Detailed error information
  • Request cancellation: Cancel pending requests
  • Progress tracking: Monitor uploads/downloads
  • TypeScript support: Full type safety

In the next lesson, we'll explore TanStack Query, which builds on Axios's capabilities by adding sophisticated caching, automatic refetching, and state management for server data.