API call in JavaScript & JS frameworks

Interacting with external applications is a necessary thing when we work on front-end applications by API calls. APIs allow different software applications to communicate with each other, enabling developers to access and retrieve data from various sources. There were many ways to API calls, like fetchAPI, XMLHttpRequest (xhr) & etc.

Here we will discuss fetching API calls and some tools for API calls.

API call using fetch API

The Fetch API provides an interface for fetching resources (including across the network). It is a more powerful and flexible replacement for XMLHttpRequest.

The Fetch API provides a JavaScript interface for accessing and manipulating parts of the protocol, such as requests and responses. It also provides a global fetch() method that provides an easy, logical way to fetch resources asynchronously across the network.

Example of fetch API

Here is a simple API call using fetch API

const url = "https://jsonplaceholder.typicode.com/todos"

// using async-await
async function getData(){
    const response = await fetch(url)
    const data = await response.json()
    return data

// using then method
function getData() {
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.log(error))

I have shown two types of API calling methods, in this article we will use the first one, to how we catch the error in the first method, Okay, for that we have to wrap the API call with a try-catch block like this.

const url = "https://jsonplaceholder.typicode.com/todos"

// using async-await
async function getData(){
        const response = await fetch(url)
        const data = await response.json()
        return data
        // return the error or throw the error

Let's call another API call with more options.

const url = "https://jsonplaceholder.typicode.com/todos"

async function postData(data = {}) {
  // Default options are marked with *
  const response = await fetch(url, {
    method: "POST", // *GET, POST, PUT, DELETE, etc.
    mode: "cors", // no-cors, *cors, same-origin
    cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
    credentials: "same-origin", // include, *same-origin, omit
    headers: {
      "Content-Type": "application/json",
      "Accept": "application/json"
    redirect: "follow", // manual, *follow, error
    referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
    body: JSON.stringify(data), // body data type must match "Content-Type" header
  return response.json() // parses JSON response into native JavaScript objects

const data = await postData({ title: 'Update the API call' })
console.log(data) // JSON data parsed by `response.json()` call
// {"title": "Update the API call", "id": 201}

You can pass your header by the example above or you can also pass a header instance

const myHeaders = new Headers({
  "Content-Type": "text/plain",
  "Accept": "application/json"
  "X-Custom-Header": "ProcessThisImmediately",

Canceling a request

Sometimes we need to cancel an API call after calling the API, To abort incomplete fetch() operations, fetch-API can do it easily with the AbortController and AbortSignal interfaces.

const controller = new AbortController()
const signal = controller.signal
const url = "https://jsonplaceholder.typicode.com/todos"

const fetchBtn = document.querySelector("#fetch-todos")
const abortBtn = document.querySelector("#abort")

fetchBtn.addEventListener("click", async () => {
  try {
    const response = await fetch(url, { signal })
    console.log("Fetch complete", response)
  } catch (error) {
    console.error(`Fetch error: ${error.message}`)

abortBtn.addEventListener("click", () => {
  console.log("Fetch aborted")

In the current world, all the API responses are within a few milliseconds so you can test this API call by modifying your network interceptor to slow 3G connection.

Upload file using fetch API

In modern web applications, there must be file upload requirements, so how we can do it with fetch API, for the file upload methodology we have to add the data as FormData is an example below.

async function upload(formData) {
  try {
    const response = await fetch("https://example.com/profile/avatar", {
      method: "PUT",
      body: formData,
    const result = await response.json();
    console.log("Success:", result);
  } catch (error) {
    console.error("Error:", error);

const formData = new FormData()
const fileField = document.querySelector('input[type="file"]')

formData.append("username", "msar")
formData.append("avatar", fileField.files[0])


Enough for fetch API if you want to learn more about the fetch API you can browse MDN documentation or JavaScript.info docs.

Alternate to fetch API - Axios

There were many tools for API calls, Axios is a great alternative tool for API calls in JS, and Axios is a simple promise-based HTTP client for the browser and node.js. Axios provides a simple-to-use library in a small package with a very extensible interface.

Loading the Axios library

To load the Axios library you can add this line to your HTML page <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> or you can add the package via npm/yarn npm install axios or yarn add axios in this article we use as yarn package.

Basic example to send an API call using Axios.

import axios from 'axios'

// Make a request for a user with a given ID
  .then(function (response) {
    // handle success
  .catch(function (error) {
    // handle error
  .finally(function () {
    // always executed

// Want to use async/await? Add the `async` keyword to your outer function/method.
async function getUser() {
  try {
    const response = await axios.get('/user?ID=12345')
  } catch (error) {

In this example, the API request will send an API call to this URL, /user

Digging deeper with Axios

We can call the APIs via Axios in many ways, we can pass a full config to the Axios instance

const base_url "https://jsonplaceholder.typicode.com"
// Send a POST request
  method: 'post',
  url: `${base_url}/users`,
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'

// Send a GET request (default method)
// GET request without options or anything.

// the structure of axios
axios(url[, config])

here is a list of available methods Axios provides.

Request method aliases

For convenience, aliases have been provided for all supported request methods.

axios.get(url[, config])
axios.delete(url[, config])
axios.head(url[, config])
axios.options(url[, config])
axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
axios.patch(url[, data[, config]])
axios.postForm(url[, data[, config]])
axios.putForm(url[, data[, config]])
axios.patchForm(url[, data[, config]])

Customizing Axios

We can create multiple instances for the Axios and use it as our necessity.
we can create an instance, and use it in our next API calls.

const instance = axios.create({
  baseURL: 'https://jsonplaceholder.typicode.com',
  headers: {
      'Accept': 'application/json'
  transformRequest: [function (data, headers) {
    // Do whatever you want to transform the data
    return data;
  // `transformResponse` allows changes to 
  // the response data to be made before
  // it is passed to then/catch
  transformResponse: [function (data) {
    // Do whatever you want to transform the data
    return data;

// API call
    .then(response => {

// response will be
// [
//   {
//     "userId": 1,
//     "id": 1,
//     "title": "delectus aut autem",
//     "completed": false
//   },
//   {
//     "userId": 1,
//     "id": 2,
//     "title": "quis ut nam facilis et officia qui",
//     "completed": false
//   },
// ......

Now we can just call the rest of the path, we don't need to pass the whole URLs,

transformRequest will transform our request config or data before calling the API, transformResponse will transform the response after successfully calling the API.
more config can be found here. We can create a global instance, so every time we use the axios we do not need to pass the config on each request, we can do that by,

axios.defaults.baseURL = 'https://jsonplaceholder.typicode.com';
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
axios.defaults.headers.post['Content-Type'] = 'application/json';

So in this process, we can use the axios method directly as below.

    .then(response => {

In Axios, we can use interceptors, for the request and response.

// Add a request interceptor
axios.interceptors.request.use(function (config) {
    // Do something before request is sent
    return config;
  }, function (error) {
    // Do something with request error
    return Promise.reject(error);

// Add a response interceptor
axios.interceptors.response.use(function (response) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    return response;
  }, function (error) {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    return Promise.reject(error);

If you see we log the response.data - not the response, so what contains the response property, the response property contains the data as below.

  // `data` is the response that was provided by the server
  data: {},

  // `status` is the HTTP status code from the server response
  status: 200,

  // `statusText` is the HTTP status message from the server response
  // As of HTTP/2 status text is blank or unsupported.
  // (HTTP/2 RFC: https://www.rfc-editor.org/rfc/rfc7540#section-
  statusText: 'OK',

  // `headers` the HTTP headers that the server responded with
  // All header names are lower cased and can be accessed using the bracket notation.
  // Example: `response.headers['content-type']`
  headers: {},

  // `config` is the config that was provided to `axios` for the request
  config: {},

  // `request` is the request that generated this response
  // It is the last ClientRequest instance in node.js (in redirects)
  // and an XMLHttpRequest instance in the browser
  request: {}

Axios provides a handy configuration for request cancelation as before we saw the fetch API abort controller.

const controller = new AbortController();

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token,
  signal: controller.signal
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error

axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token

// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');
// OR
controller.abort(); // the message parameter is not supported

More details about cancel tokens can be found here.

Okay, let's deep dive into the API calling process, The above two processes can be used anywhere in a javascript application. But what if we use those in a React application or any front-end application? Okay now let's talk about it.

API call in a react application

There are many ways to call the API in a React application.

We can use useEffect hook and useState to store the data an example below.

const [data, setData] = useState(null);

useEffect(() => {
      .then(response => response.json())
      .then(json => setData(json))
      .catch(error => console.error(error));
}, []);

In this process, the problem is if we have to call the data again in another component we can't do that, yes if we separate the logic to a custom hook it can be possible.

const usePosts = ({ per_page = 10, page = 1 }) => {
    const [data, setData] = useState(null);

    useEffect(() => {
            .then((response) => response.json())
            .then((json) => setData(json))
            .catch((error) => console.error(error));
    }, []);

    return data;

But every time we call the hook the same API will be called as our data is the same, we can prevent it by saving the first API-called data to a global store or something else. In this process we can't get the metadata like the loading state, fetching state or maybe the re-fetch functionality, then we have to create all the things from scratch.
But there were many smooth and good ways to do that, like.

  • useSWR hook by Vercel

  • ReactQuery by tanstack

Okay, let's call our API with that.

Vercel useSWR hook

SWR is a handy and lightweight tool to manage these API calls and data fetching.

SWR features as their website

  • Lightweight

  • Realtime

  • Suspense

  • Pagination

  • Backend Agnostic

  • SSR / SSG Ready

  • TypeScript Ready

  • Remote + Local

To use SWR in our application first, we have to install it.
yarn add swr after successfully installed we can use it like this.

import useSWR from 'swr'

function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher)

  if (error) return <div>failed to load</div>
  if (isLoading) return <div>loading...</div>
  return <div>hello {data.name}!</div>

Here fetcher could be the Axios instance or the fetch instance.

Let's make a hook, so our SWR does not need to pass the key every time.

import useSWR from "swr";

export function useUser(userId) {
    const { data, error, isLoading } = useSWR(
      "/api/user", // key
      () => fetch(`/api/user/${userId}`).then((res) => res.json()) //fetcher

    return {
      user: data,

So what is it?

Okay, here in the useSWR hooks' first attribute is the unique key for the request, and the second one is the fetcher, there are other attribute options which is optional, so the SWR looks like this.

const { data, error, isLoading, isValidating, mutate } = useSWR(key, fetcher, options)

By default, key will be passed to fetcher as the argument. So the following 3 expressions are equivalent:

useSWR('/api/user', () => fetcher('/api/user'))
useSWR('/api/user', url => fetcher(url))
useSWR('/api/user', fetcher)

In options, we can pass a custom config for the request.

Options receive these properties.

  • suspense = false: enable React Suspense mode

  • fetcher(args): the fetcher function

  • revalidateIfStale = true: automatically revalidate even if there is stale data

  • revalidateOnMount: enable or disable automatic revalidation when the component is mounted

  • revalidateOnFocus = true: automatically revalidate when the window gets focused

  • revalidateOnReconnect = true: automatically revalidate when the browser regains a network connection (via navigator.onLine) (details)

  • refreshInterval (details):

    • Disabled by default: refreshInterval = 0

    • If set to a number, the polling interval in milliseconds

    • If set to a function, the function will receive the latest data and should return the interval in milliseconds

  • refreshWhenHidden = false: polling when the window is invisible (if refreshInterval is enabled)

  • refreshWhenOffline = false: polling when the browser is offline (determined by navigator.onLine)

  • shouldRetryOnError = true: retry when fetcher has an error

  • dedupingInterval = 2000: dedupe requests with the same key in this time span in milliseconds

  • focusThrottleInterval = 5000: only revalidate once during a time span in milliseconds

  • loadingTimeout = 3000: timeout to trigger the onLoadingSlow event in milliseconds

  • errorRetryInterval = 5000: error retry interval in milliseconds

  • errorRetryCount: max error retry count

  • fallback: a key-value object of multiple fallback data (example)

  • fallbackData: initial data to be returned (note: This is per-hook)

  • keepPreviousData = false: return the previous key's data until the new data has been loaded (details)

  • onLoadingSlow(key, config): callback function when a request takes too long to load (see loadingTimeout)

  • onSuccess(data, key, config): callback function when a request finishes successfully

  • onError(err, key, config): callback function when a request returns an error

  • onErrorRetry(err, key, config, revalidate, revalidateOps): handler for error retry

  • onDiscarded(key): callback function when a request is ignored due to race conditions

  • compare(a, b): comparison function used to detect when returned data has changed, to avoid spurious rerenders. By default, stable-hash is used.

  • isPaused(): function to detect whether pause revalidations, will ignore fetched data and errors when it returns true. Returns false by default.

  • use: array of middleware functions

More details can be found here.

Instead of passing the configuration, we can use a global configuration for the SWR.

import useSWR, { SWRConfig } from 'swr'

function Dashboard () {
  const { data: events } = useSWR('/api/events')
  const { data: projects } = useSWR('/api/projects')
  const { data: user } = useSWR('/api/user', { refreshInterval: 0 }) // override

  // ...

function App () {
  return (
        refreshInterval: 3000,
        fetcher: (resource, init) => fetch(resource, init).then(res => res.json())
      <Dashboard />

In return, the hooks give us

  • data: data for the given key resolved by the fetcher (or undefined if not loaded)

  • error: error thrown by fetcher (or undefined)

  • isLoading: if there's an ongoing request and no "loaded data". Fallback data and previous data are not considered "loaded data"

  • isValidating: if there's a request or revalidation loading

  • mutate(data?, options?): function to mutate the cached data

Axios example

import axios from 'axios'

const fetcher = url => axios.get(url).then(res => res.data)

function App () {
  const { data, error } = useSWR('/api/data', fetcher)
  // ...

For data mutation, SWR provides a mutate method to mutate the cached data

import useSWR from 'swr'

function Profile () {
  const { data, mutate } = useSWR('/api/user', fetcher)

  const handleClick = async () => {
        const newName = data.name.toUpperCase()
        // send a request to the API to update the data
        await requestUpdateUsername(newName)
        // update the local data immediately and revalidate (refetch)
        // NOTE: key is not required when using useSWR's mutate as it's pre-bound
        mutate({ ...data, name: newName })

  return (
      <h1>My name is {data.name}.</h1>
      <button onClick={handleClick}>Uppercase my name!</button>

More about mutation can be found here.


Use null or pass a function as key to conditionally fetch data. If the function throws or returns a false value, SWR will not start the request.

// conditionally fetch
const { data } = useSWR(shouldFetch ? '/api/data' : null, fetcher)

// ...or return a falsy value
const { data } = useSWR(() => shouldFetch ? '/api/data' : null, fetcher)

// ...or throw an error when user.id is not defined
const { data } = useSWR(() => '/api/data?uid=' + user.id, fetcher)


SWR provides the preload API to prefetch the resources programmatically and store the results in the cache. preload accepts key and fetcher as the arguments.

You can call preload even outside of React.

import { useState } from 'react'
import useSWR, { preload } from 'swr'

const fetcher = (url) => fetch(url).then((res) => res.json())

// Preload the resource before rendering the User component below,
// this prevents potential waterfalls in your application.
// You can also start preloading when hovering the button or link, too.
preload('/api/user', fetcher)

function User() {
  const { data } = useSWR('/api/user', fetcher)

export default function App() {
  const [show, setShow] = useState(false)
  return (
      <button onClick={() => setShow(true)}>Show User</button>
      {show ? <User /> : null}

There were beautiful docs for SWR, you can look at them for more details.

Okay, SWR is the simplest way to handle the API request, but maybe you need something more than you must use the react-query to handle your API calls.

React Query

Powerful asynchronous state management for TS/JS, React, Solid, Vue and Svelte

Toss out that granular state management, manual re-fetching and endless bowls of async-spaghetti code. TanStack Query gives you declarative, always-up-to-date auto-managed queries and mutations that directly improve both your developer and user experiences.

React Query is often described as the missing data-fetching library for React, but in more technical terms, it makes fetching, caching, synchronizing and updating server state in your React applications a breeze.

#Enough talk, show me some code already!

import { QueryClient, QueryClientProvider, useQuery } from 'react-query'

const queryClient = new QueryClient()

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />

function Example() {
  const { isLoading, error, data } = useQuery('repoData', () =>
    fetch('https://api.github.com/repos/tannerlinsley/react-query').then(res =>

  if (isLoading) return 'Loading...'

  if (error) return 'An error has occurred: ' + error.message

  return (
      <strong>👀 {data.subscribers_count}</strong>{' '}
      <strong>✨ {data.stargazers_count}</strong>{' '}
      <strong>🍴 {data.forks_count}</strong>

So let's install the react-query

npm i react-query
# or
yarn add react-query

A quick start

Here is a quick start example.
This example very briefly illustrates the 3 core concepts of React Query:

import {
} from 'react-query'
import { getTodos, postTodo } from '../my-api'

// Create a client
const queryClient = new QueryClient()

function App() {
  return (
    // Provide the client to your App
    <QueryClientProvider client={queryClient}>
      <Todos />

function Todos() {
  // Access the client
  const queryClient = useQueryClient()

  // Queries
  const query = useQuery('todos', getTodos)

  // Mutations
  const mutation = useMutation(postTodo, {
    onSuccess: () => {
      // Invalidate and refetch

  return (
        {query.data.map(todo => (
          <li key={todo.id}>{todo.title}</li>

        onClick={() => {
            id: Date.now(),
            title: 'Do Laundry',
        Add Todo

render(<App />, document.getElementById('root'))

These three concepts make up most of the core functionality of React Query. The next sections of the documentation will go over each of these core concepts in great detail.

Let's describe this.

At first, we have to create a QueryClient instance and provide it to the QueryClientProvider and the QueryClientProvider should be on top of the application so we can use the client inside the components.

So in the Todos component you see there were some hooks from the react-query we used, useQueryClient gives us the instance of the react-query client initiated by the provider, useQuery is the actual query hook that we will use each time and the useMutation hook is to revalidate the data after a successful update or delete operation.

More about useQuery hook

A query is a declarative dependency on an asynchronous source of data that is tied to a unique key. A query can be used with any Promise-based method (including GET and POST methods) to fetch data from a server.

To subscribe to a query in your components or custom hooks, call the useQuery hook with at least:

  • A unique key for the query

  • A function that returns a promise that:

    • Resolves the data, or

    • Throws an error

const result = useQuery('todos', fetchTodoList)

The result object contains a few very important states you'll need to be aware of to be productive. A query can only be in one of the following states at any given moment:

  • isLoading or status === 'loading' - The query has no data and is currently fetching

  • isError or status === 'error' - The query encountered an error

  • isSuccess or status === 'success' - The query was successful and data is available

  • isIdle or status === 'idle' - The query is currently disabled (you'll learn more about this in a bit)

Beyond those primary states, more information is available depending on the state of the query:

  • error - If the query is in an isError state, the error is available via the error property.

  • data - If the query is in a success state, the data is available via the data property.

  • isFetching - In any state, if the query is fetching at any time (including background refetching) isFetching will be true.

For most queries, it's usually sufficient to check for the isLoading state, then the isError state, then finally, assume that the data is available and render the successful state:

function Todos() {
  const { isLoading, isError, data, error } = useQuery('todos', fetchTodoList)

  if (isLoading) {
    return <span>Loading...</span>

  if (isError) {
    return <span>Error: {error.message}</span>

  // We can assume by this point that `isSuccess === true`
  return (
      {data.map(todo => (
        <li key={todo.id}>{todo.title}</li>

The Query Key

The query key can be a string, array, or object.
If something is dependable you should include it as a query key.

function Todos({ todoId }) {
  const result = useQuery(['todos', todoId], () => fetchTodoById(todoId))

// Example of key
// A list of todos
useQuery('todos', ...) // queryKey === ['todos']

// Something else, whatever!
useQuery('somethingSpecial', ...) // queryKey === ['somethingSpecial']

// An individual todo
useQuery(['todo', 5], ...)
// queryKey === ['todo', 5]

// An individual todo in a "preview" format
useQuery(['todo', 5, { preview: true }], ...)
// queryKey === ['todo', 5, { preview: true }]

// A list of todos that are "done"
useQuery(['todos', { type: 'done' }], ...)
// queryKey === ['todos', { type: 'done' }]

Query Functions

A query function can be literally any function that returns a promise. The promise that is returned should either resolve the data or throw an error.

useQuery(['todos'], fetchAllTodos)
useQuery(['todos', todoId], () => fetchTodoById(todoId))
useQuery(['todos', todoId], async () => {
  const data = await fetchTodoById(todoId)
  return data
useQuery(['todos', todoId], ({ queryKey }) => fetchTodoById(queryKey[1]))
// throughing an error
useQuery(['todos', todoId], async () => {
  const response = await fetch('/todos/' + todoId)
  if (!response.ok) {
    throw new Error('Network response was not ok')
  return response.json()

Using a Query Object instead of parameters

Anywhere the [queryKey, queryFn, config] signature is supported throughout React Query's API, you can also use an object to express the same configuration:

import { useQuery } from 'react-query'

  queryKey: ['todo', 7],
  queryFn: fetchTodo,

react-query is a huge feature list that is used as needed, as it is a large library to handle the API calls I think this basic is enough for now, to more details and dive I will share another individual article for this, IA.

for now, you can look at the documentation of react-query and the tkdodo blogs.


In conclusion, JavaScript offers a plethora of options for making API calls, each with its own advantages and specific use cases. The Fetch API provides a built-in method for fetching resources across the web, offering a straightforward and modern interface. Axios, on the other hand, simplifies the process of making HTTP requests with additional features like interceptors and support for older browsers. For React applications, libraries like useSWR and react-query enhance data fetching by providing caching, revalidation, and a more streamlined approach to handling asynchronous data.

Ultimately, the choice of which method or library to use depends on the project requirements, developer preferences, and the specific functionalities needed. Understanding the nuances and strengths of each approach empowers developers to efficiently retrieve and manage data, ensuring a smoother and more responsive user experience in their web applications.