import axios from 'axios'; import type { APIEnvelope } from '../lib/types'; import { useAuthStore } from '../stores/authStore'; /** * Typed error class for API errors, providing structured error handling * across the application. */ export class ApiError extends Error { constructor(message: string) { super(message); this.name = 'ApiError'; } } /** * Central Axios instance configured for the Stock Data Backend API. * - Base URL: /api/v1/ * - Timeout: 30 seconds * - JSON content type */ const apiClient = axios.create({ baseURL: '/api/v1/', timeout: 120_000, headers: { 'Content-Type': 'application/json' }, }); /** * Request interceptor: attaches JWT Bearer token from the auth store * to every outgoing request when a token is available. */ apiClient.interceptors.request.use((config) => { const token = useAuthStore.getState().token; if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); /** * Response interceptor: * - Success path: unwraps the { status, data, error } envelope, returning * only the `data` field. Throws ApiError if envelope status is 'error'. * - Error path: handles 401 by clearing auth and redirecting to login. * All other errors are wrapped in ApiError with a descriptive message. */ apiClient.interceptors.response.use( (response) => { const envelope = response.data as APIEnvelope; if (envelope.status === 'error') { throw new ApiError(envelope.error ?? 'Unknown API error'); } // Return unwrapped data — callers receive the inner payload directly. // We override the response shape here; downstream API functions cast as needed. response.data = envelope.data; return response; }, (error) => { if (axios.isAxiosError(error) && error.response?.status === 401) { useAuthStore.getState().logout(); window.location.href = '/login'; } const msg = error.response?.data?.error ?? error.message ?? 'Network error'; throw new ApiError(msg); }, ); export default apiClient;