Replace axios with a simple custom fetch wrapper

I remember being with Matt Zabriskie when he
hatched the idea of a vanilla JavaScript version of AngularJS’s $http service.
It seemed like a brilliant idea and that night in his hotel room at MidwestJS,
he put together the first iteration.

It was awesome because working with raw
XMLHttpRequest
to make HTTP requests was not very fun. His library, which he later called
axios is a brilliant work and functioned
both in NodeJS and the Browser which I remember him being really excited about
(and I was too).

It’s been almost six years now and if you’re reading this chances are you’ve at
least heard of it and very likely used it in the past or are using it now. It
has an enormous and growing number of
downloads on npm. And while Matt’s long moved
on from the project, it is still actively maintained.

Since it was released, the browser standard has evolved to add a new,
promise-based API for making HTTP requests that provided a much nicer developer
experience. This API is called
fetch and if you
haven’t used it yet, you really ought to check it out. It’s widely supported and
easily polyfillable (my favorite is
unfetch because the dog mascot is cute
🐶).

Here are a few reasons you might consider swapping axios for a simple custom
wrapper around fetch:

  1. Less API to learn
  2. Smaller bundle size
  3. Reduced trouble when updating packages/managing breaking changes
  4. Immediate bug fixes/releases
  5. Conceptually simpler

I have a fetch wrapper for
my bookshelf app which has served me
well. Let’s build it together:

function client(endpoint, customConfig) {
  const config = {
    method: 'GET',
    ...customConfig,
  }

  return window
    .fetch(`${process.env.REACT_APP_API_URL}/${endpoint}`, config)
    .then(response => response.json())
}

This client function allows me to make calls to my app’s API like so:

client(`books?query=${encodeURIComponent(query)}`).then(
  data => {
    console.log('here are the books', data.books)
  },
  error => {
    console.error('oh no, an error happened', error)
  },
)

However, the built-in window.fetch API doesn’t handle errors in the same way
axios does. By default, window.fetch will only reject a promise if the
actual request itself failed (network error), not if it returned a “Client error
response”. Luckily, the Response object has
an ok property
which we can use to reject the promise in our wrapper:

function client(endpoint, customConfig = {}) {
  const config = {
    method: 'GET',
    ...customConfig,
  }

  return window
    .fetch(`${process.env.REACT_APP_API_URL}/${endpoint}`, config)
    .then(async response => {
      if (response.ok) {
        return await response.json()
      } else {
        const errorMessage = await response.text()
        return Promise.reject(new Error(errorMessage))
      }
    })
}

Great, now our promise chain will reject if the response is not ok.

The next thing we want to do is be able to send data to the backend. We can do
this with our current API, but let’s make it easier:

function client(endpoint, {body, ...customConfig} = {}) {
  const headers = {'Content-Type': 'application/json'}
  const config = {
    method: body ? 'POST' : 'GET',
    ...customConfig,
    headers: {
      ...headers,
      ...customConfig.headers,
    },
  }
  if (body) {
    config.body = JSON.stringify(body)
  }

  return window
    .fetch(`${process.env.REACT_APP_API_URL}/${endpoint}`, config)
    .then(async response => {
      if (response.ok) {
        return await response.json()
      } else {
        const errorMessage = await response.text()
        return Promise.reject(new Error(errorMessage))
      }
    })
}

Sweet, so now we can do stuff like this:

client('login', {body: {username, password}}).then(
  data => {
    console.log('here the logged in user data', data)
  },
  error => {
    console.error('oh no, login failed', error)
  },
)

Next we want to be able to make authenticated requests. There are various
approaches for doing this, but here’s how I do it in the bookshelf app:

const localStorageKey = '__bookshelf_token__'

function client(endpoint, {body, ...customConfig} = {}) {
  const token = window.localStorage.getItem(localStorageKey)
  const headers = {'Content-Type': 'application/json'}
  if (token) {
    headers.Authorization = `Bearer ${token}`
  }
  const config = {
    method: body ? 'POST' : 'GET',
    ...customConfig,
    headers: {
      ...headers,
      ...customConfig.headers,
    },
  }
  if (body) {
    config.body = JSON.stringify(body)
  }

  return window
    .fetch(`${process.env.REACT_APP_API_URL}/${endpoint}`, config)
    .then(async response => {
      if (response.ok) {
        return await response.json()
      } else {
        const errorMessage = await response.text()
        return Promise.reject(new Error(errorMessage))
      }
    })
}

So basically if we have a token in localStorage by that key, then we add the
Authorization header (per the JWT spec) which our server
can then use to determine whether the user is authorized. Very common practice
there.

Another handy thing that we can do is if the response.status is 401, that
means the user’s token is invalid (maybe it expired or something) so we can
automatically log the user out and refresh the page for them:

const localStorageKey = '__bookshelf_token__'

function client(endpoint, {body, ...customConfig} = {}) {
  const token = window.localStorage.getItem(localStorageKey)
  const headers = {'content-type': 'application/json'}
  if (token) {
    headers.Authorization = `Bearer ${token}`
  }
  const config = {
    method: body ? 'POST' : 'GET',
    ...customConfig,
    headers: {
      ...headers,
      ...customConfig.headers,
    },
  }
  if (body) {
    config.body = JSON.stringify(body)
  }

  return window
    .fetch(`${process.env.REACT_APP_API_URL}/${endpoint}`, config)
    .then(async response => {
      if (response.status === 401) {
        logout()
        window.location.assign(window.location)
        return
      }
      if (response.ok) {
        return await response.json()
      } else {
        const errorMessage = await response.text()
        return Promise.reject(new Error(errorMessage))
      }
    })
}

function logout() {
  window.localStorage.removeItem(localStorageKey)
}

Depending on your situation, maybe you’d re-route them to the login screen
instead.

On top of this, the bookshelf app has a few other wrappers for making requests.
Like the list-items-client.js:

import {client} from './api-client'

function create(listItemData) {
  return client('list-items', {body: listItemData})
}

function read() {
  return client('list-items')
}

function update(listItemId, updates) {
  return client(`list-items/${listItemId}`, {
    method: 'PUT',
    body: updates,
  })
}

function remove(listItemId) {
  return client(`list-items/${listItemId}`, {method: 'DELETE'})
}

export {create, read, remove, update}

Axios does a LOT for you and if you’re happy with it then feel free to keep
using it (I use it for node projects because it’s just great and I haven’t been
motivated to investigate the alternatives that I’m sure you’re dying to tell me
all about right now). But for the browser, I think that you’ll often be better
off making your own simple wrapper around fetch that does exactly what you
need it to do and no more. Anything you can think of for an axios interceptor or
transform to do, you can make your wrapper do it. If you don’t want it applied
to all requests, then you can make a wrapper for your wrapper.

Good luck!




Source link

مدونة تقنية تركز على نصائح التدوين ، وتحسين محركات البحث ، ووسائل التواصل الاجتماعي ، وأدوات الهاتف المحمول ، ونصائح الكمبيوتر ، وأدلة إرشادية ونصائح عامة ونصائح