Fetch API & Axios — Production Patterns
The built-in Fetch API is powerful but verbose. Axios adds interceptors, automatic JSON parsing, timeout handling, and cleaner error objects. Both are viable — understanding their production patterns (base instances, interceptors, request cancellation) is what matters.
Axios Production Setup — Instance & Interceptors
import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios';
// lib/api.ts — create a configured Axios instance
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL ?? 'http://localhost:3001/api',
timeout: 10_000, // 10 second timeout
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor — attach auth token to every request
api.interceptors.request.use((config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor — handle token refresh on 401
let isRefreshing = false;
let failedQueue: Array<{ resolve: (token: string) => void; reject: (error: unknown) => void }> = [];
api.interceptors.response.use(
response => response,
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// Queue other requests while token is being refreshed
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then(token => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return api(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const { data } = await axios.post('/api/auth/refresh', {
refreshToken: localStorage.getItem('refresh_token'),
});
localStorage.setItem('access_token', data.accessToken);
failedQueue.forEach(({ resolve }) => resolve(data.accessToken));
failedQueue = [];
originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
return api(originalRequest);
} catch (refreshError) {
failedQueue.forEach(({ reject }) => reject(refreshError));
failedQueue = [];
localStorage.clear();
window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
// Typed API functions
export const userApi = {
getAll: () => api.get<User[]>('/users').then(r => r.data),
getById: (id: string) => api.get<User>(`/users/${id}`).then(r => r.data),
create: (data: CreateUserDto) => api.post<User>('/users', data).then(r => r.data),
update: (id: string, data: UpdateUserDto) => api.patch<User>(`/users/${id}`, data).then(r => r.data),
delete: (id: string) => api.delete(`/users/${id}`),
};
export default api;Request Cancellation with AbortController
// Fetch — AbortController (built-in)
async function searchUsers(query: string, signal: AbortSignal): Promise<User[]> {
const response = await fetch(`/api/users?q=${encodeURIComponent(query)}`, { signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
// In a component — cancel on every new query
useEffect(() => {
const controller = new AbortController();
searchUsers(query, controller.signal)
.then(setResults)
.catch(err => { if (err.name !== 'AbortError') setError(err.message); });
return () => controller.abort(); // cancel previous request on next render
}, [query]);
// Axios — cancellation via AbortController (v1.x+)
useEffect(() => {
const controller = new AbortController();
api.get('/users', { signal: controller.signal })
.then(r => setData(r.data))
.catch(err => { if (!axios.isCancel(err)) setError(err.message); });
return () => controller.abort();
}, []);Common Mistakes — Fetch & Axios
- Not checking `response.ok` with Fetch — Fetch only rejects on network errors; a 404 or 500 response is still 'successful' from Fetch's perspective. Always check `if (!response.ok) throw new Error(response.statusText)`
- Forgetting to cancel requests — without AbortController, a response from a stale request can update state after the component unmounts, causing a React warning and potential bugs
- Hardcoding the base URL — always use `import.meta.env.VITE_API_URL` so it changes per environment without code changes
- Not handling the token refresh race condition — multiple 401 responses at once can trigger multiple refresh calls; the interceptor pattern above queues them
Tip
Tip
Practice Fetch API Axios Production Patterns in small, isolated examples before integrating into larger projects. Breaking concepts into small experiments builds genuine understanding faster than reading alone.
Fetch does NOT reject on 404/500. Check response.ok.
Practice Task
Note
Practice Task — (1) Write a working example of Fetch API Axios Production Patterns from scratch without looking at notes. (2) Modify it to handle an edge case (empty input, null value, or error state). (3) Share your solution in the Priygop community for feedback.
Quick Quiz
Common Mistake
Warning
A common mistake with Fetch API Axios Production Patterns is skipping edge case testing — empty inputs, null values, and unexpected data types. Always validate boundary conditions to write robust, production-ready react code.
Key Takeaways
- The built-in Fetch API is powerful but verbose.
- Not checking `response.ok` with Fetch — Fetch only rejects on network errors; a 404 or 500 response is still 'successful' from Fetch's perspective. Always check `if (!response.ok) throw new Error(response.statusText)`
- Forgetting to cancel requests — without AbortController, a response from a stale request can update state after the component unmounts, causing a React warning and potential bugs
- Hardcoding the base URL — always use `import.meta.env.VITE_API_URL` so it changes per environment without code changes