antoinette

Which HTTP Client should I use?

A guide to Javascript HTTP clients in 2025

A SoFetch themed Mean Girls meme

One of the most common tasks your Javascript/Typescript code will perform is sending HTTP requests, whether it's to a local API or a third party service. HTTP clients have changed a lot since the days of AJAX and JQuery but choosing the right client is still an important decision. Here's your 2025 guide to HTTP clients.

In This Article

We'll break down everthing you need to know about HTTP communication in 2025. Our deep dive will:

  • Introduce three powerful web clients - SoFetch from Antoinette, the built-in Fetch library, and the well-established Axios library.
  • Explore real-world programming scenarios to show each client's strengths and weaknesses
  • Provide a side-by-side comparison to help you make a well-informed choice.
  • Finally we'll offer our recommendation for an HTTP Client which matches your use case.

TLDR

HTTP clients are the unsung heroes of web devleopment. Here's what you need to know:

  • When choosing a client, consider compatibility, code clarity, and minimizing boilerplate.
  • SoFetch is a compact, zero‑boilerplate HTTP client that excels at communicating with web services and APIs in just a few lines of code.
  • Axios is a mature library with many features, but it may be overkill for simple apps.
  • The Fetch API is native to browsers and Node. It's ideal out of the box, though you may write more boilerplate.

The key is finding a library that matches your project requirements - read on for the lowdown on each client.

What is an HTTP Client?

HTTP (HyperText Transfer Protocol) defines how computers talk over the web. Each time you load a website, your browser uses HTTP to request resources from a remote server. Often you'll write TypeScript code that sends and receives data from a URL - for that you need an HTTP client.

All HTTP clients:

  • Send data to a URL
  • Receive data back from the URL
  • Send metadata (headers) that tell the server extra information, for example:
    • The content type being sent
    • The content type expected in the response
    • Authentication or identity data to secure the request

What is SoFetch?

SoFetch is an HTTP client that simplifies sending and receiving data via HTTP. It uses sensible defaults to minimize boilerplate, so you only write code relevant to your application. Since JSON APIs dominate in 2025, SoFetch defaults to JSON for requests and responses and supports type‑safe usage with minimal configuration.

What is Fetch?

Fetch is the standard HTTP client shipped with modern browsers and Node. It's unopinionated, stable, and flexible, but you may write extra boilerplate for common patterns (JSON, error handling, auth, etc.), which higher‑level clients often abstract.

What is Axios?

Before Fetch became ubiquitous, Axios was a widely used HTTP client. It is mature, well‑documented, and feature rich. For large projects Axios can be a good fit, but it adds significant bundle size (~2 MB unpacked).

How do Fetch, SoFetch and Axios compare?

Below are three common scenarios (POST request, error handling, and bearer authentication) comparing SoFetch, vanilla Fetch, and Axios.

Example data

const loginRequest = {
	username: "Regina.George",
	password: "soFetch1234",
	stayLoggedIn: true
}

interface LoginResponse {
	authToken: string,
	expiresTimestamp: number
}

A POST request

SoFetch

In SoFetch (without any configuration) the request would be a single line of code:

const loginResponse = await soFetch<LoginResponse>("/api/login", loginRequest) 

By default soFetch assumes:

  • That since we supplied a second argument the request is a POST request.*
  • The request will be in JSON format
  • The response will also be in JSON format
  • The response will be of type LoginResponse

(*A request to soFetch with only one argument are assumed to be a GET request but don't worry - you can make PUT, PATCH and DELETE requests too)

Fetch

Let's compare this to the same request using Fetch:

const requestBody = JSON.stringify(loginRequest)
const response = await fetch("/api/login", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(loginRequest),
  });
)
if (!response.ok) {
    throw new Error(`Login failed: ${response.status} ${response.statusText}`);
}
const data: LoginResponse = await response.json();
return data;

soFetch throws an error for non-2XX (error) response codes by default, but with Fetch you'll need to handle the error yourself. In Fetch you'll also need to write some more boilerplate to specify a POST request in JSON format, and explicitly parse the JSON response.

Axios

How does Axios compare for the same request?

const loginResponse = await axios.post<LoginResponse>("/api/login", loginRequest).data;

This is much more like soFetch since Axios also serialises the request and adds the "Content-Type": "application/json" header automatically, but you still need to specify the request type and the .data property.

Error Handling

We can expand our previous example by handling some possible error response. Let's assume that we'll want to specifically handle:

401 Unauthorized - the user has supplied the wrong password

404 Not found - the user was not found

and have a generic handler for all other errors.

Let's compare SoFetch, Fetch and Axios for this expanded example:

SoFetch

const loginResponse = await soFetch<LoginResponse>("/api/login", loginRequest)
	.catchHttp(Status.Unauthorized401, () => alert("Username/password incorrect"))
	.catchHttp(Status.NotFound404, () => alert("User not found"))
	.catch(() => alert("An unexpected error occurred")

Here we can use the SoFetch catchHttp handler to handle a specific error response and a regular catch() to handle all other errors. We're also using the SoFetch Status enum here to make the code easier to understand, but we could also just use an integer to specify the error code

Fetch

const response = await fetch("/api/login", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(loginRequest),
})

if (response.status === 401) {
  alert("Username/password incorrect")
}

if (response.status === 404) {
  alert("User not found")
}

if (!response.ok) {
  alert("An unexpected error occurred")
}

const data: LoginResponse = await response.json()
return data

In regular Fetch we'll need to check the status code after we get a response but before we serialise the response body.

Axios

try {
    const response = await axios.post<LoginResponse>("/api/login", loginRequest);
    return response.data;
  } catch (error) {
    if (axios.isAxiosError(error)) {
      const status = error.response?.status;
      if (status === 401) {
        alert("Username/password incorrect")
      }
      if (status === 404) {
        alert("User not found")
      }
      throw new Error(
        alert("An unexpected error occurred")
      );
    } else {
      alert("An unexpected error occurred")
    }
  }
}

We're having to retrieve the status code here and check for both response errors and network errors (which is why the line alert("An unexpected error occurred") is in two places.

Bearer Authentication

For our final example, let's imagine that we're communicating with an API that uses Bearer Tokens and that we want to persist a token using local storage. Authenticating our HTTP requests will consist of two parts:

  • Telling our HTTP Client to attach a bearer token to our requests from LocalStorage if such a token exists
  • Persisting our token in LocalStorage

Using soFetch

soFetch.useBearerAuthentication()
//Then, once we've received a bearer token from a successful login:
soFetch.setAuthToken("MY_GROOVY_AUTH_TOKEN")

By default when you call useBearerAuthentication() on the browser soFetch assumes:

  • That you want to use bearer authentication with your requests
  • You want to persist the token using LocalStorage
  • The token will be stored under the key SOFETCH_AUTHENTICATION

(As with all the soFetch authentication helpers, you can also pass an object that allows you to specify the persistence method and the authentication key, if you don't want to use SOFETCH_AUTHENTICATION. Persistence using localStorage, sessionStorage, cookies and a custom function are all supported, or you can hold the authentication key in memory if you prefer.)

Using vanilla Fetch

Fetch is pretty unopinionated out of the box, so you'd need to roll your own client to create similar functionality. It might look something like:

export class HttpClient {
  private getHeaders(): HeadersInit {
    const headers: HeadersInit = {
      "Content-Type": "application/json",
    };

    const token = localStorage.getItem("BEARER_TOKEN");
    if (token) {
      headers["Authorization"] = `Bearer ${token}`;
    }

    return headers;
  }

  async request<T>(
    endpoint: string,
    options: RequestInit = {}
  ): Promise<T> {
    const response = await fetch(`${endpoint}`, {
      ...options,
      headers: {
        ...this.getHeaders(),
        ...(options.headers || {}),
      },
    });

    if (!response.ok) {
      throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
    }

    return response.json() as Promise<T>;
  }
}

and then to set the Bearer Token you'd just do:

localStorage.setItem("MY_GROOVY_AUTH_TOKEN")

Axios

Using Axios you could implement similar functionality with something like:

axios.interceptors.request.use((config) => {
  // Add Content-Type header if missing
  config.headers = config.headers ?? {};
  if (!config.headers["Content-Type"]) {
    config.headers["Content-Type"] = "application/json";
  }

  // Attach bearer token from localStorage if present
  const token = localStorage.getItem("BEARER_TOKEN");
  if (token) {
    config.headers["Authorization"] = `Bearer ${token}`;
  }

  return config;
});

//Add the bearer token after login using:
localStorage.setItem("MY_GROOVY_AUTH_TOKEN")

Side-by-Side

FeatureSoFetchFetchAxios
FootprintAdditional dependency (277 kB)Built-in to modern browsers and Node(18+)Additional dependency (2.24 MB)
Installationnpm i @antoinette-agency/sofetchNo installation needednpm i axios
ReadabilityVery readable, conciceWell structured, but can be verboseReadable, occasionally verbose
JSON HandlingAutomaticManualAutomatic (via response.data)
Error handlingThrows on non‑2xx; Fluent, expressive API. Access to full rejection responseOnly network error throws. HTTP 4xx/5xx are resolved — manual checks neededRejects on non‑2xx;
Authentication- Built-in helper methods cover majority of modern authentication methods.
- Built-in token persistence.
- Authentication via interceptors for more complex cases.
None built-inVia interceptors
Typescript SupportExcellentBasicExcellent

Conclusion

Choosing an HTTP client in 2025 isn't about sticking with your favourite library for all your projects. It's more about choosing the right tool for the job - each one has a sweet spot.

When to Choose Each Client

  • Minimal Project - keep it simple and choose Fetch for zero overhead and native support. Perfect for prototypes.
  • Larger Project - select Axios for it's robust feature set and extensive ecosystem.
  • Modern, Lean Devlepment - choose SoFetch for an optimal combination of simplicity and power. The best choice for developers prioritizing code readability and minimal boilerplate.

Pro tip: Don't be afraid to mix approaches. In particular SoFetch is a minimal layer over the Fetch API so they play nicely together. There's nothing to stop you from using both.

The HTTP Client landscape continues to evolve so make sure you review your choices periodically. The client you're using today might not be the best choice for your next project.

Let's make your website:
Cost Less.
Run Faster.

Hi, we're Antoinette.

We help our clients improve performance, reduce costs and save money. Get in touch to see how we could help you.

By clicking the button above, you consent for Antoinette to contact you at the email address provided. Privacy policy

ABOUT ANTOINETTE
a

Antoinette: a digital agency specialising in site performance, rebuilds and headless CMS.

We help our clients reduce their hosting costs, improve site speed and save money. We believe the practices that improve your codebase also improve your bottom line.

Antoinette is based in Brighton, UK

Stay In The Loop
Sign-up for our newsletter and keep things up to date.

By clicking the button above, you consent for Antoinette to contact you at the email address provided. Privacy policy

© 2025 AntoinettePrivacy

Antoinette uses cookies to improve site performance.