Lesson 4.5: Axios - Alternative to Fetch
Duration: 60 minutes
Learning Objectives
By the end of this lesson, you will be able to:
- Install and configure Axios in a TypeScript project
- Make HTTP requests using Axios
- Understand the benefits of Axios over native Fetch
- Configure Axios instances with defaults
- Use interceptors for request/response modification
- 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:
- Log all requests
- Add a timestamp to requests
- 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
- Axios provides a cleaner API than Fetch with less boilerplate
- Automatic JSON parsing - no need to call
.json() - Automatic error throwing for HTTP errors (4xx, 5xx)
- Instances let you configure reusable API clients
- Interceptors enable request/response transformation
- Use isAxiosError() for type-safe error handling
- TypeScript generics provide excellent type inference
- 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.