High Five Studio

June 2026

Why I Switched from Axios to Native fetch() for Croatian APIs

Why I replaced Axios with native fetch() for Croatian APIs—cutting bundle size while gaining control and performance

Why I Switched from Axios to Native fetch() for Croatian APIs

I’ve been building web apps in Croatia for over a decade, and for most of that time, Axios was my go‑to HTTP client. It handled interceptors, transformed JSON automatically, and felt like the safe choice. But recently, I took a hard look at my bundle sizes, the growing complexity of my dependency trees, and how Croatian APIs (from tax authority endpoints to local payment gateways) actually behave in production. That’s when I decided to strip away the library and go back to the browser’s native fetch().

The shift wasn’t just about shaving off kilobytes. It was about control, performance, and understanding exactly what happens between my code and the API. Here’s why I made the switch, what I learned in the process, and how you can do it too without losing your sanity.

The Real Cost of Axios in a Croatian Web Project

When you’re working with Croatian APIs—whether it’s the FINA e‑Račun system, a local bank’s REST service, or a custom API from a Zagreb‑based startup—every millisecond and every kilobyte counts. Axios, for all its convenience, comes with baggage.

First, there’s the bundle size. Axios is around 14 KB gzipped. That doesn’t sound like much, but on a slow mobile connection in a coastal town where 3G is still common, every byte matters. Native fetch() is already in the browser, so you pay exactly zero for it.

Second, Axios wraps the native fetch() under the hood anyway (since version 1.x). You’re essentially adding an abstraction layer on top of something the browser already gives you for free. For a project that calls a handful of Croatian APIs, that abstraction often adds more complexity than it solves.

Third, there’s the maintenance overhead. Every time Axios releases a new version, you have to test your integration. With native fetch(), you’re immune to third‑party breaking changes. Your code relies on a stable browser API that’s been supported in all modern browsers for years.

How Native fetch() Handles Croatian API Quirks

Croatian APIs have their own personality. They’re not always as polished as Stripe or Twilio. Some return XML instead of JSON, others expect specific headers for authentication, and many have inconsistent error handling. Native fetch() gives you the raw control to deal with these quirks without fighting a library.

Dealing with Non‑Standard Status Codes

I once worked with a Croatian payment gateway that returned HTTP 200 for both success and failure, with the actual status buried in the response body. Axios’s default behavior would treat any 2xx as success and throw for 4xx/5xx. That meant I had to write custom interceptors just to handle this one API.

With fetch(), you don’t get that automatic error‑throwing. You check response.ok yourself. For that payment gateway, I wrote a simple wrapper:

async function apiCall(url, options = {}) {
  const response = await fetch(url, options);
  const data = await response.json();
  if (!response.ok && !data.success) {
    throw new Error(data.error_message || 'API error');
  }
  return data;
}

That’s it. No interceptor setup, no extra library. Just a function that handles the exact behavior of that API.

Handling XML Responses from Croatian Government APIs

The Croatian tax authority’s e‑Porezna system still uses XML for some endpoints. Axios can handle XML, but you need to add responseType: 'document' or a custom transformer. With fetch(), you get the raw text or blob directly, then parse it with DOMParser or a lightweight XML library. It’s one less dependency.

const response = await fetch('https://e-porezna.porezna-uprava.hr/api/...');
const xmlText = await response.text();
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, 'text/xml');

No Axios, no extra library. Just the browser’s built‑in parser.

Rewriting the Interceptor Pattern with Pure Functions

One of the biggest selling points of Axios is its interceptor system. You can add a request interceptor to attach an auth token, and a response interceptor to handle 401s globally. It’s elegant—but it’s also opaque. When something breaks, you have to debug the interceptor chain.

Native fetch() doesn’t have interceptors, but you don’t need them. You can achieve the same pattern with a simple wrapper function that composes smaller functions.

Building a Custom fetch() Wrapper

Here’s a concrete example from a project I built for a Croatian e‑commerce client. The app needed to call an inventory API, attach a JWT token from localStorage, and automatically refresh the token when it expired.

async function authenticatedFetch(url, options = {}) {
  const token = localStorage.getItem('access_token');
  const headers = {
    'Content-Type': 'application/json',
    ...options.headers,
    Authorization: token ? `Bearer ${token}` : '',
  };

  let response = await fetch(url, { ...options, headers });

  if (response.status === 401) {
    const refreshToken = localStorage.getItem('refresh_token');
    const refreshResponse = await fetch('/api/refresh', {
      method: 'POST',
      body: JSON.stringify({ refresh_token: refreshToken }),
    });
    if (refreshResponse.ok) {
      const { access_token } = await refreshResponse.json();
      localStorage.setItem('access_token', access_token);
      headers.Authorization = `Bearer ${access_token}`;
      response = await fetch(url, { ...options, headers });
    } else {
      // Redirect to login
      window.location.href = '/login';
    }
  }

  if (!response.ok) {
    const error = await response.json().catch(() => ({}));
    throw new Error(error.message || `HTTP ${response.status}`);
  }

  return response.json();
}

This is pure, testable, and doesn’t hide the logic inside a library. You can see exactly what happens at every step. When a Croatian API returns a 401 with a custom error body, you handle it right there.

Performance Gains That Matter for Croatian Users

Croatian internet infrastructure is improving, but we still have significant variance between urban and rural areas. Users in Split or Zagreb might have fiber, but those on the islands or in Lika often rely on mobile data with high latency.

Smaller Bundle Means Faster First Paint

By removing Axios, I cut my initial JavaScript bundle by about 14 KB. That might not seem huge, but on a 3G connection, that’s an extra second of parsing and execution time. For a user in a remote part of Dalmatia, that second can be the difference between a usable app and a frustrating experience.

Native fetch() also runs in the browser’s fetch API, which is implemented in C++ and optimized by the browser vendor. Axios runs in JavaScript on top of that. Every request with Axios has an extra layer of abstraction that adds micro‑latency. When you’re making multiple API calls per page load (e.g., fetching product data, user info, and localized content), those microseconds add up.

No More CORS Preflight Headaches

Many Croatian APIs have strict CORS policies. Axios sometimes sends extra headers (like X-Requested-With) that trigger preflight OPTIONS requests. Native fetch() by default sends a simple request unless you add custom headers. If your API doesn’t need custom headers, you avoid an entire round trip.

In one project with a Croatian logistics API, removing the default X-Requested-With header that Axios sent cut the API call time in half because the server didn’t have to handle a preflight request.

The Developer Experience: What You Lose and What You Gain

Let’s be honest—switching to native fetch() isn’t all sunshine. There are genuine trade‑offs, and ignoring them would be dishonest.

What You Lose

  • Automatic JSON parsing: Axios does response.data for you. With fetch(), you always call response.json() or response.text(). It’s one extra line per call.
  • Request cancellation: Axios has built‑in cancellation with CancelToken (or AbortController in newer versions). Native fetch() requires you to manually create an AbortController and pass its signal.
  • Progress events: Axios supports upload progress via onUploadProgress. Native fetch() doesn’t have a built‑in way to track upload progress (though you can use the XMLHttpRequest streaming approach or ReadableStream for downloads).
  • Timeout handling: Axios has a timeout option. With fetch(), you need to combine it with AbortSignal.timeout() or a manual timeout wrapper.

What You Gain

  • Zero‑dependency confidence: Your HTTP layer depends only on the browser. No npm audit warnings, no breaking changes from a library maintainer.
  • Native streaming: fetch() supports streaming responses natively. You can start processing data as it arrives, which is great for large responses from Croatian APIs that return paginated data.
  • Transparent debugging: When something goes wrong, you open DevTools and see the exact fetch() call. No abstraction layers to step through.
  • Smaller learning curve for new team members: Junior developers in Croatia often know fetch() from their first JavaScript tutorials. Axios requires learning a new API.

A Concrete Anecdote

Last year, I was helping a small agency in Osijek migrate a legacy Angular app. The app was using Axios 0.21, which had a known vulnerability. The upgrade to Axios 1.x broke half the interceptors because the API had changed significantly. We spent three days refactoring Axios code.

When we finally switched to native fetch(), we deleted the entire Axios configuration file and replaced it with a single 40‑line wrapper function. The app became faster, the codebase was cleaner, and the team could finally understand the HTTP layer without reading Axios documentation.

How to Make the Switch Without Breaking Everything

If you’re convinced and want to migrate an existing project, here’s a practical plan.

Step 1: Abstract Your API Calls

If you’re already using Axios directly everywhere, start by creating a single wrapper function (like the one I showed above) that all your API calls go through. This is good practice regardless of which library you use.

// api.js
export async function get(url) {
  const response = await fetch(url);
  if (!response.ok) throw new Error(`GET ${url} failed`);
  return response.json();
}

Then replace every axios.get(url) with get(url). Do this incrementally, file by file.

Step 2: Handle Edge Cases

  • Timeouts: Use AbortSignal.timeout(5000) (available in modern browsers) or a polyfill.
  • Cancellation: Create an AbortController and pass its signal to fetch().
  • Upload progress: For file uploads, consider using XMLHttpRequest directly or a small dedicated library like file‑uploader that wraps progress events.

Step 3: Test Thoroughly

Test with the actual Croatian APIs you use. Some might behave differently with native fetch() (e.g., sending cookies, handling redirects). Use DevTools to compare request headers between Axios and fetch().

Step 4: Remove Axios from package.json

Once every call is migrated, run npm uninstall axios. Then run your test suite. If everything passes, you’re done.

A Forward‑Looking Note on Croatian Web Development

The web platform is maturing. APIs like fetch(), AbortController, and ReadableStream are now stable and well‑supported. The trend in modern frontend development is toward “vanilla” solutions that rely on the platform rather than third‑party abstractions.

For developers in Croatia, this is especially relevant. We often build for smaller budgets and tighter timelines. Every library we add is a risk—a potential breaking change, a security vulnerability, or a performance penalty. By using native fetch(), you’re making a bet on the browser’s stability rather than a library’s maintenance schedule.

I’m not saying Axios is bad. It’s a well‑written library that solved real problems when fetch() didn’t exist or was inconsistent. But those days are over. The browser has caught up.

Next time you start a new project—or refactor an old one—try writing your HTTP layer without any library. Start with a simple fetch() call. Add a wrapper for your specific needs. You’ll be surprised how little you actually need, and how much cleaner your code becomes.

And when you’re debugging a weird response from a Croatian API at 2 AM, you’ll appreciate knowing exactly what’s happening in every line of your HTTP code—without having to dig into a third‑party library’s internals.