Skip to content

[READY] Replace axios with native fetch wrapper#1109

Open
Sharqiewicz wants to merge 5 commits intostagingfrom
chore/remove-axios
Open

[READY] Replace axios with native fetch wrapper#1109
Sharqiewicz wants to merge 5 commits intostagingfrom
chore/remove-axios

Conversation

@Sharqiewicz
Copy link
Copy Markdown
Member

Summary

  • Replace axios with native fetch across the entire codebase - frontend services, shared package (SquidRouter, dynamic EVM tokens)
  • Introduce ApiError class and isApiError type guard as typed replacements for axios.isAxiosError and AxiosError
  • Wire AbortSignal from TanStack Query's queryFn context through to fetch() - queries for fiat accounts, ramp history, and fee comparison now cancel in-flight requests on unmount or stale invalidation
  • Replace SquidRouter's axios instance with a dedicated squidFetch helper that separates HTTP error handling from response validation, preserving HTTP status codes on thrown errors

@Sharqiewicz Sharqiewicz requested a review from ebma April 2, 2026 09:45
@netlify
Copy link
Copy Markdown

netlify bot commented Apr 2, 2026

Deploy Preview for vortexfi ready!

Name Link
🔨 Latest commit cbeeae1
🔍 Latest deploy log https://app.netlify.com/projects/vortexfi/deploys/69ce5df3bf712000086264de
😎 Deploy Preview https://deploy-preview-1109--vortexfi.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link
Copy Markdown

netlify bot commented Apr 2, 2026

Deploy Preview for vortex-sandbox ready!

Name Link
🔨 Latest commit cbeeae1
🔍 Latest deploy log https://app.netlify.com/projects/vortex-sandbox/deploys/69ce5df31fbae60008b048ab
😎 Deploy Preview https://deploy-preview-1109--vortex-sandbox.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates the codebase from axios to a native fetch-based HTTP layer, introducing typed error handling and improving request cancellation behavior (via TanStack Query AbortSignal).

Changes:

  • Replaced axios usage with fetch in shared SquidRouter + dynamic token fetch paths.
  • Introduced a frontend api-client wrapper (apiFetch/apiRequest) with ApiError + isApiError for typed error handling.
  • Wired TanStack Query AbortSignal through to relevant frontend API calls (fee comparison, ramp history, fiat accounts).

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/shared/src/tokens/evm/dynamicEvmTokens.ts Switch SquidRouter token retrieval from axios to fetch with basic HTTP status handling.
packages/shared/src/services/squidrouter/route.ts Replace axios-based route/status calls with a squidFetch helper and custom HTTP error type.
packages/shared/package.json Remove axios dependency from shared package.
package.json Remove root axios dependency.
bun.lock Remove axios entries from the lockfile.
apps/frontend/src/services/api/ramp.service.ts Add optional AbortSignal to ramp history API call and forward to apiRequest.
apps/frontend/src/services/api/price.service.ts Add optional AbortSignal to price API call and forward to apiRequest.
apps/frontend/src/services/api/monerium.service.ts Migrate axios error checks to isApiError and adjust response handling to new client return shape.
apps/frontend/src/services/api/auth.api.ts Update auth calls to use the new apiClient return shape (data directly).
apps/frontend/src/services/api/api-client.ts Replace axios client with fetch wrapper, add ApiError + isApiError, and update apiRequest.
apps/frontend/src/services/api/alfredpay.service.ts Update Alfredpay service methods to return data directly from apiClient; add AbortSignal to fiat accounts listing.
apps/frontend/src/sections/individuals/FeeComparison/FeeComparisonTable/hooks/useFeeComparisonData.ts Pass React Query signal down to price service.
apps/frontend/src/hooks/useRampHistory.ts Pass React Query signal down to ramp history service.
apps/frontend/src/hooks/alfredpay/useFiatAccounts.ts Pass React Query signal down to Alfredpay fiat accounts listing.
apps/frontend/package.json Remove axios dependency from frontend package.
apps/api/package.json Remove axios dependency from API package.
Comments suppressed due to low confidence (1)

apps/frontend/src/hooks/useRampHistory.ts:43

  • The queryFn now receives an AbortSignal, but aborts will be caught and swallowed by the broad catch, causing the query to resolve successfully with partial/empty data instead of being cancelled. Consider detecting abort (signal.aborted or error.name === "AbortError") and rethrowing so React Query can treat it as a cancellation.
    queryFn: async ({ signal }) => {
      const allTransactions: Transaction[] = [];

      for (const address of addresses) {
        try {
          const response = await RampService.getRampHistory(address, 100, undefined, signal);
          const transactions = response.transactions.map(formatTransaction);
          allTransactions.push(...transactions);
        } catch (error) {
          console.warn(`Failed to fetch wallet history for ${address}:`, error);
        }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +4 to +12
export class ApiError extends Error {
status: number;
data: { error?: string; message?: string; details?: string };

/**
* Base API client for making requests to the backend
*/
export const apiClient: AxiosInstance = axios.create({
baseURL: `${SIGNING_SERVICE_URL}/v1`,
headers: {
"Content-Type": "application/json"
},
timeout: 30000
});
constructor(status: number, data: Record<string, unknown>, message: string) {
super(message);
this.status = status;
this.data = data;
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ApiError.data is declared as { error?: string; message?: string; details?: string }, but the constructor accepts Record<string, unknown> and assigns it directly. This is not type-safe (and likely fails TS assignability because unknown isn’t assignable to string). Consider typing data as Record<string, unknown> (or making ApiError generic) and extracting error/message/details via a helper when needed.

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +49
const response = await fetch(url.toString(), {
body: isFormData ? (options.data as FormData) : options.data !== undefined ? JSON.stringify(options.data) : undefined,
headers: {
...(tokens?.accessToken ? { Authorization: `Bearer ${tokens.accessToken}` } : {}),
...(!isFormData ? { "Content-Type": "application/json" } : {}),
...options.headers
},
method,
signal: options.signal ?? AbortSignal.timeout(30000)
});
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signal: options.signal ?? AbortSignal.timeout(30000) changes timeout semantics: when a caller provides a signal, the 30s timeout is no longer applied (axios previously always enforced it). Also, AbortSignal.timeout isn’t supported in all browsers/environments. Consider combining signals (e.g., timeout + caller signal via AbortSignal.any or a manual AbortController) and falling back when AbortSignal.timeout is unavailable.

Copilot uses AI. Check for mistakes.
Comment on lines +4 to 66
export class ApiError extends Error {
status: number;
data: { error?: string; message?: string; details?: string };

/**
* Base API client for making requests to the backend
*/
export const apiClient: AxiosInstance = axios.create({
baseURL: `${SIGNING_SERVICE_URL}/v1`,
headers: {
"Content-Type": "application/json"
},
timeout: 30000
});
constructor(status: number, data: Record<string, unknown>, message: string) {
super(message);
this.status = status;
this.data = data;
}
}

export function isApiError(error: unknown): error is ApiError {
return error instanceof ApiError;
}

// Add request interceptor for common headers and auth token
apiClient.interceptors.request.use(
config => {
// Add Authorization header if user is authenticated
const tokens = AuthService.getTokens();
if (tokens?.accessToken) {
config.headers.Authorization = `Bearer ${tokens.accessToken}`;
async function apiFetch<T>(
method: string,
path: string,
options: {
data?: unknown;
params?: Record<string, string | number | boolean | undefined>;
headers?: Record<string, string>;
signal?: AbortSignal;
} = {}
): Promise<T> {
const tokens = AuthService.getTokens();

const url = new URL(`${SIGNING_SERVICE_URL}/v1${path}`);
if (options.params) {
for (const [key, value] of Object.entries(options.params)) {
if (value !== undefined) url.searchParams.set(key, String(value));
}
return config;
},
error => {
return Promise.reject(error);
}
);

// Add response interceptor for error handling
apiClient.interceptors.response.use(
response => {
return response;
},
(error: AxiosError) => {
console.error("API Error:", error.response?.data || error.message);
return Promise.reject(error);
const isFormData = options.data instanceof FormData;

const response = await fetch(url.toString(), {
body: isFormData ? (options.data as FormData) : options.data !== undefined ? JSON.stringify(options.data) : undefined,
headers: {
...(tokens?.accessToken ? { Authorization: `Bearer ${tokens.accessToken}` } : {}),
...(!isFormData ? { "Content-Type": "application/json" } : {}),
...options.headers
},
method,
signal: options.signal ?? AbortSignal.timeout(30000)
});

if (!response.ok) {
const errorData = (await response.json().catch(() => ({}))) as { error?: string; message?: string };
console.error("API Error:", errorData);
throw new ApiError(response.status, errorData, errorData.error ?? errorData.message ?? response.statusText);
}
);

/**
* Helper function to handle API errors
* @param error The error object
* @param defaultMessage Default error message
* @returns Formatted error message
*/
if (response.status === 204) return undefined as T;
return response.json() as Promise<T>;
}

export const handleApiError = (error: unknown, defaultMessage = "An error occurred"): string => {
if (axios.isAxiosError(error)) {
const responseData = error.response?.data as { error?: string; message?: string; details?: string } | undefined;
return responseData?.error || responseData?.message || error.message || defaultMessage;
if (isApiError(error)) {
return error.data?.error ?? error.data?.message ?? error.message ?? defaultMessage;
}
return error instanceof Error ? error.message : defaultMessage;
};
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new client now throws ApiError (with status/data) rather than an axios error shape (error.response.status/data). There are still call sites in the frontend that inspect err.response to branch on status/body, which will break with this change. Either update those call sites or consider exposing a backward-compatible shape/helper to reduce migration risk.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants