5
\$\begingroup\$

I have created a wrapper for Fetch to send requests to my API. The module exports api, and requests can be made with:

api.get({ url: '/users' })

or

api.post({ url: '/users', data: {id: 1, name: myName} })

After receiving the HTTP response from fetch I do some additional tasks, like update a token, then extract the JSON, camelCase the JSON keys, then return the JSON.

I'm mostly interested in a review of the api export, and handle errors in the fetch request.

const baseURL = 'http://mydomain.com/'

const csrfTokenExtractor = (response) => {
  const token = response.headers['x-csrf-token']
  if (token) {
    document.querySelector('meta[name=csrf-token]').setAttribute('content', token)
  }
  return response
}

const csrfHeader = () => {
  const token = document.querySelector('meta[name=csrf-token]').getAttribute('content')
  return {
    'X-CSRF-Token': token,
  }
}

const defaultHeader = () => {
  return {
    'X-App-Component': 'app',
  }
}

const sessionDetector = (response) => {
  if (response.status === 401) {
    window.location.replace(response.data.url)
  }
  return response
}

const buildURLQuery = (obj) =>
  Object.entries(obj)
    .map((pair) => pair.map(encodeURIComponent).join('='))
    .join('&')

const request = async ({ url, method, ...params }) => {
  params.credentials = 'same-origin'
  params.headers = Object.assign({}, params.headers || {}, defaultHeader())
  params.method = method

  if (method !== 'GET') {
    params.headers = Object.assign({}, params.headers || {}, { 'Content-Type': 'application/json' }, csrfHeader())
  }

  let response

  try {
    response = await fetch(`${baseURL}${url}`, { 
...params })

    if (!response.ok) {
      console.error(response)
      throw response
    }
  } catch (error) {
    sessionDetector(error)
    console.error(error)
    throw error
  }

  await csrfTokenExtractor(response)
  const json = await response.json()
  const formattedJson = camelcaseKeys(json, { deep: true })

  return formattedJson
}

const api = {
  get: ({ url, formData = {} }) => {
    const options = {
      method: 'GET',
      mode: 'cors',
      url: `${url}?${buildURLQuery(formData)}`,
    }

    return request(options)
  },

  post: ({ url, data = {} }) => {
    const options = {
      method: 'POST',
      url,
      body: JSON.stringify(data),
    }

    return request(options)
  },

  put: ({ url, data = {} }) => {
    const options = {
      method: 'PUT',
      url,
      body: JSON.stringify(data),
    }

    return request(options)
  },

  delete: ({ url }) => {
    const options = {
      method: 'DELETE',
      url,
    }

    return request(options)
  },

  request,
}

export default api
\$\endgroup\$
4
\$\begingroup\$

A few things stood out to me:

  1. You can extract out the implementation details behind where the token exists. Not a huge deal, but personally I would do this:

    const setAuthToken = (token) => document.querySelector('meta[name=csrf-token]').setAttribute('content', token)
    const getAuthToken = () =>document.querySelector('meta[name=csrf-token]').getAttribute('content')
    
  2. I don't think you should throw an error for a non-ok response. It's not a fatal error, so I don't think you should treat it as such.

    if (!response.ok) {
        console.error(response)
        // throw response
        return Promise.reject(response)
    }
    
  3. Be careful with the headers. The fetch api can also accept a Headers object and you can't merge it in with Object.assign. Headers is iterable, which is nice so you can use a for...of loop to manually extract the keys and set them into your object

    Read more about the Headers interface of the Fetch API here

\$\endgroup\$
2
  • \$\begingroup\$ Regarding #2: I'm no expert in Promises, but don't throw response and return Promise.reject(response) do exactly the same thing in this context? \$\endgroup\$ – RoToRa Jul 16 '20 at 8:05
  • \$\begingroup\$ @RoToRa In most situations, yes they are functionally the same. But the expressiveness and intent are different. While this is starting to creep into a philosophical discussion, should a successful api call with an undesirable response from a 3rd party be considered a fatal error on the callee's part? In all honesty, I wouldn't even consider throwing or rejecting in an ok === false condition. There should just be user feedback stating something went wrong. \$\endgroup\$ – Andrew Jul 16 '20 at 19:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.