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 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:
Using AbortController (Recommended)
'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.