DEV Community

Nikolas ⚡️
Nikolas ⚡️

Posted on • Originally published at nikolasbarwicki.com

Why do people use Axios instead of Fetch

Axios vs. Fetch

In the dynamic realm of JavaScript and front-end development, selecting the appropriate tool for HTTP requests is critical. Axios and Fetch stand out as two leading contenders, each offering distinct features and benefits. This article delves into their differences and practical applications, providing a comprehensive comparison.

Data Fetching in React Using Axios

Fetching data in React with axios is a straightforward process. axios is a promise-based HTTP client for both the browser and Node.js, often lauded for its simplicity and ease of use. Here, we'll explore a practical example involving data retrieval from the Star Wars API.

Basic Implementation

Let's begin with a basic implementation using the fetchData function within a useEffect hook to fetch data upon component mount. axios performs a GET request, and upon promise resolution, the state variable data is updated with the fetched information. If data exists, it's then displayed in the UI.

export default function App() {
  const [data, setData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const response = await axios.get("https://swapi.dev/api/starship/9");
      const d = response.data;
      setData(d);
    };

    fetchData();
  }, []);

  return data ? <pre>{JSON.stringify(data)}</pre> : null;
}
Enter fullscreen mode Exit fullscreen mode

However, this basic implementation lacks essential features and is prone to errors. For a more robust solution, consider using tanstack-query, an excellent library by Tanner Linsley. It simplifies many complex tasks, offering features that are challenging to build from scratch. The final version of our code above is inspired by an article from the main maintainer of this package, TkDodo.

Enhanced Implementation

The improved version addresses the shortcomings of the initial code by handling loading states, errors, and canceling fetch operations when a component unmounts.

export default function App() {
  const [isLoading, setIsLoading] = useState(true);
  const [data, setData] = useState(null);
  const [error, setError] = useState<AxiosError | null>(null);

  useEffect(() => {
    let ignore = false;

    const fetchData = async () => {
      setIsLoading(true);

      try {
        const response = await axios.get("https://swapi.dev/api/starship/9");
        const d = response.data;

        if (!ignore) {
          setData(d);
          setError(null);
        }
      } catch (error) {
        if (!ignore) {
          setError(error as AxiosError);
          setData(null);
        }
      } finally {
        if (!ignore) setIsLoading(false);
      }
    };

    fetchData();
    return () => { ignore = true; };
  }, []);

  if (isLoading) return <span>Loading data...</span>;
  if (error) return <span>An error occurred...</span>;
  return data ? <pre>{JSON.stringify(data)}</pre> : null;
}
Enter fullscreen mode Exit fullscreen mode

For a deeper understanding of this implementation, exploring TkDodo's insightful article is recommended. However, our focus here is on implementing similar functionality without external dependencies like axios.

Data Fetching in React Using Fetch

Fetching data in React applications is a common task. Understanding the subtleties of different methods can greatly enhance your development process. In this segment, we'll delve deeper into the nuances of using fetch and compare it with axios.

Barebones Example

The simplest code to fetch data might look like this:

const response = await fetch("https://swapi.dev/api/starship/9")
const data = await response.json()
Enter fullscreen mode Exit fullscreen mode

This snippet illustrates the straightforward nature of fetch. As a native web API, it requires no additional libraries and is supported by most modern browsers. This simplicity is one of fetch's key strengths.

JSON Data Transformation with Fetch

A significant difference between axios and fetch is in handling JSON data. axios automatically transforms the response to JSON format under the hood, allowing you to use the response data directly.

const data = await response.json()
Enter fullscreen mode Exit fullscreen mode

fetch, as a lower-level approach, requires explicit conversion of responses to JSON, unlike axios's automatic handling. This requirement might seem cumbersome, but it provides a more detailed level of control over HTTP requests and responses.

Basic Error Handling

The primary distinction between fetch and axios lies in their approach to JSON data transformation and error handling. fetch requires manual conversion of the response to JSON and does not throw errors for HTTP status codes like 400 or 500. Instead, you need to check the ok status of the response, which can be seen as both a feature and a limitation, depending on your application's needs.

By default, fetch doesn't throw errors for non-200 statuses, so the code below won't behave the same as axios for "400 bad request", "404 not found", or "500 internal server error":

try {
    const response = await fetch("https://swapi.dev/api/starship/9")
    const data = await response.json()
} catch (error) {
  // Handle the error
}
Enter fullscreen mode Exit fullscreen mode

On the response object, fetch has an ok property which is a boolean. We can write a simple if statement that throws an error for non-200 statuses like this:

try {
    const response = await fetch("https://swapi.dev/api/starship/9")

    if (!response.ok) {
        throw new Error('Bad fetch response')
    }

    const data = await response.json()
} catch (error) {
  // Handle the error
}
Enter fullscreen mode Exit fullscreen mode

While this code is a start, it's far from the error handling already implemented in axios. We're now catching errors for all responses with non-200 statuses without even trying to process the response body.

Custom ResponseError

For more robust error handling, consider creating a custom ResponseError class. This approach offers more control and specificity when managing different HTTP status codes, ensuring a more resilient and user-friendly experience.

We can create a custom ResponseError suited to our use case:

class ResponseError extends Error {
    response: Response;

    constructor(message: string, response: Response) {
        super(message);
        this.response = response;
        this.name = 'ResponseError';
    }
}
Enter fullscreen mode Exit fullscreen mode

Replace the error throwing logic in our fetch routine with this:

try {
    const response = await fetch("https://swapi.dev/api/starship/9")

    if (!response.ok) {
        throw new ResponseError('Bad fetch response', response)
    }

    const data = await response.json()
} catch (error) {
    switch (error.response.status) {
      case 404: /* Handle "Not found" */ break;
      case 401: /* Handle "Unauthorized" */ break;
      case 418: /* Handle "I'm a teapot!" */ break;
      // Handle other errors
      default: /* Handle default */ break;
    }
}
Enter fullscreen mode Exit fullscreen mode

Sending POST Requests Using Fetch

While axios simplifies the process of sending POST requests by automatically stringifying the request body and setting appropriate headers, fetch requires these steps to be done manually. This grants developers more control but also adds complexity.

For fetch, you need to remember three actions:

  1. Set the method to POST,
  2. Set headers (in our case) to { "Content-Type": "application/json" } (Many backends require this, as they will not process the body properly otherwise.)
  3. Manually stringify the body using JSON.stringify() (if sending JSON, the body must be a JSON-serialized string).

Let's switch from a GET to a POST request to expand our logic:

try {
    const response = await fetch("https://swapi.dev/api/starship/9", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ "hello": "world" })
    })

    if (!response.ok) {
        throw new ResponseError('Bad fetch response', response)
    }

    const data = await response.json()
} catch (error) {
    switch (error.response.status) {
      case 404: /* Handle "Not found" */ break;
      case 401: /* Handle "Unauthorized" */ break;
      case 418: /* Handle "I'm a teapot!" */ break;
      // Handle other errors
      default: /* Handle default */ break;
    }
}
Enter fullscreen mode Exit fullscreen mode

Using Fetch with TypeScript

Incorporating TypeScript into your fetch requests brings an added layer of robustness through type safety. By defining interfaces and using generics, TypeScript ensures that your data fetching logic is more predictable and less prone to runtime errors. This practice enhances code maintainability and readability, especially in larger applications.

Type-Safe Fetch Responses

Implementing type-safe responses ensures that your application correctly handles the data structure returned by the API. This approach minimizes runtime errors and ensures consistency throughout your application.

export async function customFetch<TData>(url: string, options?: RequestInit): Promise<TData> {
    const defaultHeaders = {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
    };

    let fetchOptions: RequestInit = {
        method: 'GET', // Default method
        headers: defaultHeaders,
        ...options
    };

    // If there's a body and it's an object, stringify it
    if (fetchOptions.body && typeof fetchOptions.body === 'object') {
        fetchOptions.body = JSON.stringify(fetchOptions.body);
    }

    // Merge the default headers with any provided headers
    fetchOptions.headers = { ...defaultHeaders, ...(options?.headers || {}) };

    try {
        const response = await fetch(url, fetchOptions);

        if (!response.ok) {
            throw new ResponseError('Bad fetch response', response);
        }

        return response.json() as Promise<TData>;
    } catch (error) {
        handleFetchError(error);
    }
}

// Usage example
interface Starship {
    // Define the properties of a Starship here
}

try {
    const data = await customFetch<Starship>("https://swapi.dev/api/starship/9")
    // ... use data ...
} catch (error) {
  // ... handle error ...
}
Enter fullscreen mode Exit fullscreen mode

Typing the Rejected Value of the Promise

By typing the rejected value of the promise, you provide more precise error handling. This helps in distinguishing between different types of errors and dealing with them appropriately, enhancing the robustness of your application.

In TypeScript, by default, the error in a catch block is of any type. Because of this, our previous snippets will cause an error in our IDE:

try {
    // ...
} catch (error) {
    // 🚨 TS18046: error is of type unknown
  switch (error.response.status) {
    case 404: /* Handle "Not found" */ break
    case 401: /* Handle "Unauthorized" */ break
    case 418: /* Handle "I'm a teapot!" */ break

    // Handle other errors
    default: /* Handle default */ break;
  }
}
Enter fullscreen mode Exit fullscreen mode

We can't directly type error as ResponseError. TypeScript assumes you can't know the type of the error, as the fetch itself could throw an error other than ResponseError. Knowing this, we can have an implementation ready for nearly all errors and handle them in a type-safe manner like this:

try {
    // ...
} catch (error) {
    if (error instanceof ResponseError) {
        // Handle ResponseError
        switch (error.response.status) {
          case 404: /* Handle "Not found" */ break;
          case 401: /* Handle "Unauthorized" */ break;
          case 418: /* Handle "I'm a teapot!" */ break;
          // Handle other errors
          default: /* Handle default */ break;
        }
    } else {
        // Handle non-ResponseError errors
        throw new Error('An unknown error occurred when fetching data', {
          cause: error
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

To further enhance type safety and developer experience, you might consider using the zod package to parse the output of your customFetch. This approach is similar to what TypeScript ninja Matt Pocock does in his package, zod-fetch.

Conclusion

In this comprehensive exploration of data fetching in React, we've dissected the functionalities and nuances of axios and fetch. Both tools come with their strengths and particularities, catering to various development needs. As we wrap up, let's distill the essence of this discussion and consider practical applications.

axios shines with its straightforward syntax and automatic JSON data handling, making it a developer favorite for its simplicity and ease of use. On the other hand, fetch, being a native browser API, offers fine-grained control over HTTP requests, a boon for developers seeking a more hands-on approach.

However, as with all tools, understanding their limitations and how to overcome them is crucial. For instance, fetch's lack of automatic error handling for non-200 status responses can be a stumbling block. But with the custom ResponseError class and proper error handling mechanisms, you can significantly enhance its robustness.

Let's revisit the enhanced error handling and TypeScript integration in fetch to solidify our understanding:

class ResponseError extends Error {
    response: Response;

    constructor(message: string, response: Response) {
        super(message);
        this.response = response;
        this.name = 'ResponseError';
    }
}

function handleFetchError(error: unknown) {
    if (error instanceof ResponseError) {
        // Detailed error handling based on status code
        switch (error.response.status) {
            case 404: /* Handle "Not found" */ break;
            case 401: /* Handle "Unauthorized" */ break;
            case 418: /* Handle "I'm a teapot" */ break;
            // ... other status codes ...
            default: /* Handle other errors */ break;
        }
    } else {
        // Handle non-ResponseError errors
        throw new Error('An unknown error occurred', { cause: error });
    }
}

export async function customFetch<TData>(url: string, options?: RequestInit): Promise<TData> {
    const defaultHeaders = {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
    };

    let fetchOptions: RequestInit = {
        method: 'GET', // Default method
        headers: defaultHeaders,
        ...options
    };

    // If there's a body and it's an object, stringify it
    if (fetchOptions.body && typeof fetchOptions.body === 'object') {
        fetchOptions.body = JSON.stringify(fetchOptions.body);
    }

    // Merge the default headers with any provided headers
    fetchOptions.headers = { ...defaultHeaders, ...(options?.headers || {}) };

    try {
        const response = await fetch(url, fetchOptions);

        if (!response.ok) {
            throw new ResponseError('Bad fetch response', response);
        }

        return response.json() as Promise<TData>;
    } catch (error) {
        handleFetchError(error);
    }
}
Enter fullscreen mode Exit fullscreen mode

In this code, we see how TypeScript adds a layer of type safety and predictability. By defining a ResponseError class, we gain control over how errors are handled and presented. Furthermore, the customFetch function illustrates how to build a more robust and versatile fetch utility, one that can be tailored to various data types through generics.

For developers leaning towards TypeScript, integrating type safety into your data fetching strategy isn't just about preventing errors; it's about creating a more predictable, maintainable, and scalable codebase.

As you weigh your options between axios and fetch, consider your project's needs, your team's familiarity with these tools, and the kind of control or simplicity you're aiming for. Remember, the best tool is the one that aligns with your project's objectives and enhances your development workflow.

Lastly, for those seeking a middle ground between the simplicity of axios and the control of fetch, consider libraries like wretch. It offers a much better API and functionalities like:

  • request cancellation,
  • progress monitoring,
  • and request interception, all while maintaining a small footprint.

In conclusion, whether you choose axios, fetch, or an alternative like wretch, your focus should be on writing clear, maintainable, and robust code. Understanding the strengths and weaknesses of each tool will empower you to make informed decisions and build applications that are not only functional but also resilient and enjoyable to develop.

Top comments (10)

Collapse
 
manchicken profile image
Mike Stemle • Edited

There are a few extra aspects of the Fetch API that I think folks forget about:

  • You have to make sure to regularly update axios to keep up with security problems, but not with the Fetch API.
  • axios takes up space in your node_modules and bundle, but the Fetch API does not.
  • The Fetch API works with the built-in URL API, but axios does not.

While I know that this isn't the point of your article, I think it's important to note that String Manipulation of URLs is an Anti-Pattern.

Collapse
 
jwhenry3 profile image
Justin Henry

In many cases, the biggest argument against using fetch directly is running the same code in node and in the browser. Fetch is not accessible in node, so you would need to use a library like node-fetch in order to access similar functionality on the backend. Rather than taking this route, many REST libraries can run on both sides because they polyfill on the inside.

Collapse
 
florianrappl profile image
Florian Rappl

This is outdated. You can use fetch in recent versions.

Anyway, the argument of having a stable layer in between is still compelling.

Another argument to make is that axios gives you a better thought out API and more options - eg having a progress callback when uploading or downloading large files.

Collapse
 
jwhenry3 profile image
Justin Henry

I agree its outdated, however, many companies run with older versions of technologies and shifting the infrastructure requires jumping through hoops. In an ideal scenario, those in charge of infrastructure should keep up with the versions, but unfortunately its not always the case.

Thread Thread
 
dscheglov profile image
Dmytro Shchehlov

We got rid of node-fetch from a large nextjs-project (both server and client side) in a single commit and two files changed:

  • package.json
  • services.ts (the composition root for our DI)

The problem is not in the fetch, the problem is that view directly depends on the implementation isntead of depending on the abstraction.

Collapse
 
dscheglov profile image
Dmytro Shchehlov • Edited

So, detailed research. But I guess it contains a problem, that could be expressed with the simple question: why React components/hooks have to warry about fetch or axios?

React components MUST rely on the abstraction, that MUST be provided on the highest level as it is possible (ideally on the application level, but considering SPA architecture it could be a "page"-level or something like that).

Let's guess we need to display the Books fetched via some rest-like API.
It doesn't mean that we need to write fetch requests inside of the react-components or even hooks.

We need to create an interface (or type):

interface IBooksProvider {
  (libraryId: string): Promise<Book[]>
}
Enter fullscreen mode Exit fullscreen mode

Then we need to declare correspondent dependency inside of the react-component or hook. For simplicity let's select the context api

const booksProviderContext = React.createContext<IBooksProvider>(async () => {
  throw new ReferenceError("BookProvider is not injected");
});
Enter fullscreen mode Exit fullscreen mode

then we can use it:

type LibraryBooksProps = {
 libraryId: string;
}

type LibraryBooksState = {
  books: Book[];
  isLoading: boolean;
  error?: unknown;
}

const LibraryBooks = ({ libraryId }: LibraryBooksProps) => {
  const getBooks = React.useContext(booksProviderContext);
  const [state, setState] = React.useState<LibraryBooksState>({ 
    books: [],
    isLoading: true,
  });

  useEffect(async () => {
    setState({ books: [], isLoading: true });
    try {
       const books = await getBooks(libraryId);
       setState({ books, isLoading: false }); 
    } catch(error) {
       setState({ books: [], isLoading: false, error });
    }
  }, [getBooks, libraryId]);

  useEffect(() => {
    if (state.error) throw state.error; // we need to re-throw an async error
    // or you can just use `react-error-boundary` library instead
  }, [state.error]);

  return (state.isLoading 
     ? <Spinner> 
     : <BooksList>
        {state.books.map(
           book => <Book key={book.id} {...book} />
        )}
      </BooksList>
   );
}
Enter fullscreen mode Exit fullscreen mode

Having this code we can write tests for this components WITHOUT mocking the fetch.

Please note, that component LibraryBooks doesn't know anything about the source of the data, it is not aware of authorization, content types, http-respones, base urls, protocols etc.

Sure, we need to create function that implements the IBooksProvider interface.
But we should to do the same.
This function MUST NOT depend directly on the fetch or axios. Again, it MUST depend on the abstraction:

interface IHttpClient {
  get(path: string, options?: HttpOptions): Promise<HttpResponse>;
  // ... -- snip --
}
Enter fullscreen mode Exit fullscreen mode

Where the types HttpOptions and HttpResponse could be as narrow as it allows your project (the backend you are using, or are going to use).

Let's guess, the following:

type HttpOptions = {
  headers?: Record<string, string>;
  signal?: AbortSignal;
};

type HttpResponse = {
  status: number;
  statusText: string;
  body: unknown;
};
Enter fullscreen mode Exit fullscreen mode

Then we need to create a function getLibraryBooks:

const getLibraryBooks =
  (http: Pick<IHttpClient, "get">) =>
  (libraryId: string) =>
     http.get(`/library/${libraryId}/books`).then(
       ({ status, statusText, body }) => {
         if (status === 200) {
          // the body could be checked against some schema,
          // written with something like zod or io-ts
           return body as Book[];
         }
         if (status === 404) {
           // better to return null here, but for our
           // example let it be an empty array
           return [];
         }
         throw new Error(
           `Failed to fetch books: ${status} - ${statusText}\n${String(body)}`
         );
      }
    );
Enter fullscreen mode Exit fullscreen mode

Again, we can write tests for this function. Note, it is not aware of the base url and authorization. Also it assumes that httpClient deserializes body according to the response headers.
Please note, that for the function getLibraryBooks any response status except of 200 and 404 is unexpected, so it can just throw an error (not httpClient, but this function)

So, now we need to write an implementation for our IHttpClient.
And only now we should choose the fetch or axios. More then, this implementation MUST NOT depend on the fetch or axios-instance directly, it must depend on the abstraction, and allow to inject the correspondent instances.

The practice shows that such implementation has the same complexity for fetch and axios. We had a nextjs-project (used fetch) and pure back-end nodejs project (used axios) and we have a service that must work on both projects. So we had to implement the IHttpClient for both: fetch and axios to allow to use this Service in both projects. Later when we rewrite other services of the back-end project to use IHttpClient instead of axios, we got rid of the last one, and after node20 LTS released we removed the node-fetch from the both projects.

No react-component suffered )

Summarizing:

  1. Create a onion architecture even for Front-end, where the View Layer is not aware of the Data Layer details
  2. Use dependency injection patterns to compose the application according to the your project needs
  3. Follow SOLID even if you are a fan of the functional programming
Collapse
 
ravavyr profile image
Ravavyr

The title of this article should be modified to say "... in React".

Not all of us use Axios, because not all of us need it.
Fetch works perfectly fine in vanilla JS applications.

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

Thanks for the great post!!! A lot of details! There are also axios interceptors that are very cool to use 😊

Collapse
 
psypher1 profile image
James 'Dante' Midzi

I have never understood why people insists on installing something for things you can already natively do.

Collapse
 
gene profile image
Gene

This article is not for beginners.