Replace axios with native fetch API wrapper

Drop axios dependency in favor of a lightweight fetch-based client
that preserves the same { data: T } interface, keeping all 25
consumer files unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-04-03 20:10:48 -06:00
parent 51c106dc6c
commit 78e20f30cb
3 changed files with 72 additions and 112 deletions

View File

@@ -1,25 +1,78 @@
import axios from 'axios';
const BASE_URL = import.meta.env.VITE_API_URL || '/api/v1';
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || '/api/v1',
});
class ApiError extends Error {
response: { status: number; data: unknown };
constructor(status: number, data: unknown) {
super(`Request failed with status ${status}`);
this.response = { status, data };
}
}
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
interface RequestConfig {
params?: Record<string, string | number | boolean | undefined>;
}
api.interceptors.response.use(
(res) => res,
(err) => {
if (err.response?.status === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
async function request<T>(method: string, url: string, body?: unknown, config?: RequestConfig): Promise<{ data: T }> {
let fullUrl = `${BASE_URL}${url}`;
if (config?.params) {
const search = new URLSearchParams();
for (const [k, v] of Object.entries(config.params)) {
if (v !== undefined) search.set(k, String(v));
}
return Promise.reject(err);
const qs = search.toString();
if (qs) fullUrl += `?${qs}`;
}
const headers: Record<string, string> = {};
const token = localStorage.getItem('token');
if (token) headers['Authorization'] = `Bearer ${token}`;
let fetchBody: BodyInit | undefined;
if (body instanceof FormData || body instanceof URLSearchParams) {
fetchBody = body;
} else if (body !== undefined) {
headers['Content-Type'] = 'application/json';
fetchBody = JSON.stringify(body);
}
const res = await fetch(fullUrl, { method, headers, body: fetchBody });
if (res.status === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
throw new ApiError(401, null);
}
if (!res.ok) {
let data: unknown = null;
try { data = await res.json(); } catch {}
throw new ApiError(res.status, data);
}
if (res.status === 204) return { data: null as T };
const data = await res.json();
return { data };
}
const api = {
get<T = any>(url: string, config?: RequestConfig) {
return request<T>('GET', url, undefined, config);
},
);
post<T = any>(url: string, body?: unknown, config?: RequestConfig) {
return request<T>('POST', url, body, config);
},
patch<T = any>(url: string, body?: unknown, config?: RequestConfig) {
return request<T>('PATCH', url, body, config);
},
put<T = any>(url: string, body?: unknown, config?: RequestConfig) {
return request<T>('PUT', url, body, config);
},
delete<T = any>(url: string, config?: RequestConfig) {
return request<T>('DELETE', url, undefined, config);
},
};
export default api;
@@ -101,6 +154,7 @@ export interface Transaction {
notes: string | null;
category_id: number | null;
category: Category | null;
deferred_to_next_cycle: boolean;
created_at: string;
}
@@ -213,6 +267,7 @@ export interface MonthlyDetail {
uncovered_actual: number;
gran_total_egresos: number;
net_balance: number;
cc_by_category: { category_name: string; amount: number }[];
}
// Budget API functions