import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
headers: {
'Content-Type': 'application/json',
},
})
// Request interceptor - add auth token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('authToken')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// Add request ID for debugging
config.headers['X-Request-ID'] = uuidv4()
return config
},
(error) => Promise.reject(error)
)
// Response interceptor - handle errors
api.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
if (error.response?.status === 401) {
localStorage.removeItem('authToken')
window.location.href = '/login'
}
const errorMessage =
(error.response?.data as any)?.message ||
'An unexpected error occurred'
return Promise.reject({
message: errorMessage,
status: error.response?.status,
data: error.response?.data,
})
}
)
export default api
A centralized API client provides a single place to configure authentication, error handling, and request/response transformations. I use axios for its interceptor support and automatic JSON transformation. Request interceptors attach the JWT token from localStorage to every request's Authorization header. Response interceptors handle 401 errors by redirecting to login and parse server errors into a consistent format. I also add a request ID header for debugging and set reasonable timeouts to prevent hanging requests. The base URL points to the Rails API, configured via environment variables so it works across dev, staging, and production. This abstraction means controllers never deal with auth headers or error parsing directly.