Axios API client: configuration and usage patterns
How the centralized Axios instance is configured, how request and response interceptors handle authentication, and how shared API helpers are structured.
Use this file to discover all available pages before exploring further.
All HTTP requests in DRTC Fluvial Admin flow through a single, pre-configured Axios instance defined in lib/axios.ts. This instance sets the base URL, attaches the Bearer token on every outgoing request, and automatically handles token refresh when the API returns a 401 Unauthorized response. By centralising this logic in one place, every feature and shared API helper benefits from consistent authentication and error handling without any extra setup.
Response interceptor: token refresh and request queuing
When the API returns a 401, the response interceptor attempts a silent token refresh before retrying the original request. Requests that arrive while a refresh is already in progress are queued and replayed once the new token is available.
lib/axios.ts
let isRefreshing = false;let failedQueue: any[] = [];const processQueue = (error: any, token: string | null = null) => { failedQueue.forEach((prom) => { if (error) { prom.reject(error); } else { prom.resolve(token); } }); failedQueue = [];};axiosInstance.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; // If not 401, or request already retried, propagate the error if (error.response?.status !== 401 || originalRequest._retry) { return Promise.reject(error); } // A refresh is already in flight — queue this request if (isRefreshing) { return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }); }).then((token) => { originalRequest.headers.Authorization = `Bearer ${token}`; return axiosInstance(originalRequest); }); } originalRequest._retry = true; isRefreshing = true; try { // Call the refresh endpoint const res = await axios.post( "/api/auth/refresh", {}, { withCredentials: true } ); const newToken = res.data.access_token; if (!newToken) throw new Error("No access token returned"); localStorage.setItem("access_token", newToken); axiosInstance.defaults.headers.common.Authorization = `Bearer ${newToken}`; processQueue(null, newToken); return axiosInstance(originalRequest); } catch (err) { processQueue(err, null); localStorage.removeItem("access_token"); window.location.href = "/auth"; return Promise.reject(err); } finally { isRefreshing = false; } });
1
Request returns 401
The interceptor checks whether this request has already been retried (_retry flag). If it has, the error propagates normally.
2
Queue or refresh
If another refresh is in progress (isRefreshing === true), the request is wrapped in a Promise and pushed onto failedQueue. Otherwise, isRefreshing is set to true and the refresh call begins.
3
Refresh succeeds
The new access_token is stored in localStorage, set on the instance defaults, and passed to processQueue, which resolves every queued Promise. All queued requests then retry with the new token.
4
Refresh fails
processQueue is called with the error, rejecting all queued requests. The stale token is removed from localStorage and the user is redirected to /auth.
For cases where the native fetch API is preferred (for example, file uploads or streaming), lib/fetchWithAuth.ts provides a simpler wrapper with a single-retry refresh:
lib/fetchWithAuth.ts
export async function fetchWithAuth(url: string, options: RequestInit = {}) { let res = await fetch(`/api${url}`, { ...options, credentials: "include", }); if (res.status === 401) { const refresh = await fetch(`/api/auth/refresh`, { method: "POST", credentials: "include", }); if (refresh.ok) { res = await fetch(`/api${url}`, { ...options, credentials: "include", }); } else { window.location.href = "/auth"; } } return res;}
fetchWithAuth does not queue concurrent requests during a refresh the way the Axios interceptor does. For data-fetching in components and hooks, prefer axiosInstance.
Functions in shared/api/ are thin wrappers around axiosInstance calls. They are used across multiple features and accept typed parameters and return types.
All catalog responses share a common base interface defined in types/catalogos.ts:
types/catalogos.ts
export interface CatalogoBase { id: string; codigo: string; nombre: string;}export type TipoNaveCatalogo = CatalogoBase;export type MaterialCatalogo = CatalogoBase;export type ModalidadCatalogo = CatalogoBase;export type ServicioNave = CatalogoBase;
Each catalog type is an alias of CatalogoBase. Use the specific type alias (for example TipoNaveCatalogo) rather than CatalogoBase directly so that TypeScript error messages and IDE completions reference the correct domain name.
Feature-level API functions follow the same pattern. Here is the dashboard stats function and a minimal example of how you would call it inside a React component:
import axiosInstance from "@/lib/axios";const api = axiosInstance;export async function getDashboardStats() { const res = await api.get("/dashboard"); return res.data;}
When you create a new feature API file, place it under features/<feature>/services/ and name it <feature>.api.ts. Import axiosInstance from @/lib/axios — never create a separate Axios instance.
Use toastEmpadronamiento in vessel registration flows, toastPermisos in permit flows, and toastGeneral for authentication feedback and general-purpose notifications.