Using JavaScript’s Fetch with a REST API

If you are at all familiar with modern JavaScript, you have probably heard of, or used, Fetch; a function that allows you to make asynchronous HTTP requests. It leverages ES6 promises to make it easy to define asynchronous behavior. Fetch and promises can be tricky to work with and understand, especially if you are new to them. In this post, we will get a basic understanding of each of them, then discover how we can create a collection of wrappers to make sending requests to a traditional REST API a breeze.

Promises

Essentially, a promise is a returned object to which you attach callbacks, instead of passing callbacks into a function.

Using promises

A promise is basically an object that a function can return that will eventually resolve into a value. The most important property of a promise is that it is chainable. It has a series of methods such as then() and catch() that allow you to define behavior for when the previous promise resolves to a value. These methods also return promises, so that other behavior can be attached to them, and so on. This property of promises is essential to writing clean, generic code involving asynchronous behavior.

Fetch

Fetch is essentially a function that makes an AJAX request and returns a promise that resolves to a response object, which contains the status code, headers, body, and so on. Leveraging this behavior, we can easily write generic logic for requests to a REST API.

Create a Wrapper

Although not necessarily required for every application, I like to create a wrapper function around Fetch. The idea is that you will import and use this wrapper function instead of calling Fetch directly. This allows you to define generic behavior that should occur for every request. For example, you may want to log the user out if a 401 Unauthorized response is received:

import isoFetch from 'isomorphic-fetch';
import { logout } from './dummy.mjs';

/**
 * Wraps isomorphic-fetch and logs the user out if an unauthorized
 * response is received.
 *
 * @param {string} url      The url for the request.
 * @param {object} options  The options for the request.
 */
export default function fetch(url, options = {}) {
  return (
    isoFetch(url, options)
      .then((response) => {
        if (response.status === 401) {
          logout();
        }
        return response;
      })
  );
}

In our fetch() wrapper function, we pass the arguments through to Fetch and chain some generic logic to the returned promise with then() to log the user out, if necessary, and break the chain. You can also handle things like network errors here. Going forward, we will use this function instead of using Fetch directly.

Create Generic Functions

Now that we have a generic wrapper function around Fetch, it is time to write even more wrapper functions around it. In a traditional REST API, you are able to make GET, POST, PUT, PATCH, and DELETE requests. It may sound like a lot of work to write functions for all of these, but we can actually handle all responses very similarly.

GET

First, let’s write a function get() that can be used easily to make GET requests. We want this function to handle the details of the response and return the (promise that resolves to the) desired result or error response.

import fetch from './fetch.mjs';

export function get(url, extraOpts = {}) {
  var opts = {
    credentials: 'include',
  };
  return (
    fetch(url, Object.assign(opts, extraOpts))
      .then((response) => {
        const contentType = response.headers.get('content-type');
        if (contentType && contentType.indexOf('application/json') !== -1) {
          return response.json().then((json) => {
            if (response.ok) {
              return json;
            }

            // Error response received. Reject the promise with the appropriate message.
            const userMsg = getUserError(response, json);
            return Promise.reject(userMsg);
          });
        }

        if (!response.ok) {
          const errorMsg = `An unexpected error occured: ${response.statusText}`;
          return Promise.reject(errorMsg);
        }

        return response;
      })
  );
}

function getUserError(response, json) {
  return json.error || response.statusText;
}

Let’s break this down a little at a time.

import fetch from './fetch.mjs';

We first import the generic fetch() function we created; not Fetch.

export function get(url, extraOpts = {}) {

The function takes in a URL and extraOpts, which is an optional parameter that allows you to pass extra options or override default options to pass to Fetch.

var opts = {
  credentials: 'include',
};
return (
  fetch(url, Object.assign(opts, extraOpts))

This is where we call fetch(). The only option we are passing to Fetch by default is credentials: 'include'. This option needs to be defined if you are using cookie authentication for your REST API.

.then((response) => {
  const contentType = response.headers.get('content-type');
  if (contentType && contentType.indexOf('application/json') !== -1) {
    return response.json().then((json) => {

Here, we are chaining onto the promise returned by fetch() and defining the behavior that should occur when it resolves to a response object. First, we check to see if the response is a JSON response. If it is, we need to get and return that JSON. We resolve the body of the response as JSON by calling response.json(), which also returns a promise. By returning this promise, the parent promise (the one returned by the surrounding then()) will actually resolve to whatever this promise resolves to. Kind of confusing, huh?

if (response.ok) {
  return json;
}

// Error response received. Reject the promise with the appropriate message.
const userMsg = getUserError(response, json);
return Promise.reject(userMsg);

Inside the chained then() on response.json() we are doing a couple of things. First, we need to check if the response status code indicates success. That’s right; Fetch does not reject the promise for an error status code. Instead, you need to check response.ok. For a successful response, we want to return the JSON. For an unsuccessful response, we want to try to parse the JSON for a user-friendly error message. This is what getUserError() does, which will vary depending on the application. Finally, we return Promise.reject() to reject the promise for an unsuccessful request.

if (!response.ok) {
  const errorMsg = `An unexpected error occured: ${response.statusText}`;
  return Promise.reject(errorMsg);
}

return response;

Lastly, if the response did not contain JSON, we simply check to see if it was successful or not. If it was not, we return a rejected promise with a generic error message, since a more specific one was not provided. Otherwise, simply return the response object itself.

Use get()

We now have a function that will allow us to make GET requests quite easily.

import { get } from './api.mjs';

(
  get('https://reqres.in/api/users?page=2')
    .then((json) => {
      console.log('------------- GET 1 success -------------');
      console.log(json);
    })
    .catch((error) => {
      console.log('------------- GET 1 failure -------------');
      console.log(error);
    })
);

When calling get(), you need at least two things: a then() to handle the JSON result on success, and a catch() to handle the error message that may be returned by one of the Promise.reject() returns in get().

POST, PUT, and PATCH

When writing generic functions for the other HTTP methods, it turns out that we have already done most of the work. The response handling logic works not only for GET requests, but for all of them. Simply extract it into a handleResponse() function and update get() to use it:

export function get(url, extraOpts = {}) {
  var opts = {
    credentials: 'include',
  };
  return (
    fetch(url, Object.assign(opts, extraOpts))
      .then(handleResponse)
  );
}

function handleResponse(response) {
  const contentType = response.headers.get('content-type');
  if (contentType && contentType.indexOf('application/json') !== -1) {
    return response.json().then((json) => {
      if (response.ok) {
        return json;
      }

      // Error response received. Reject the promise with the appropriate message.
      const userMsg = getUserError(response, json);
      return Promise.reject(userMsg);
    });
  }

  if (!response.ok) {
    const errorMsg = `An unexpected error occured: ${response.statusText}`;
    return Promise.reject(errorMsg);
  }

  return response;
}

Next, we need to create functions for POST, PUT, and PATCH. The implementations for these are very similar, so we will first create a generic update() function, like so:

function update(url, method, data, extraOpts = {}) {
  var opts = {
    method,
    body: JSON.stringify(data),
    headers: {
      'Content-Type': 'application/json',
    },
    credentials: 'include',
  };
  return (
    fetch(url, Object.assign(opts, extraOpts))
      .then(handleResponse)
  );
}

There is a little more going on here than in get(). For these requests, we need to encode data as JSON and indicate that the request contains JSON via the Content-Type header. Otherwise, this works exactly the same way as get(). Now, we just need to create wrapper functions for each HTTP method.

export function post(url, data, extraOpts = {}) {
  return update(url, 'POST', data, extraOpts);
}

export function put(url, data, extraOpts = {}) {
  return update(url, 'PUT', data, extraOpts);
}

export function patch(url, data, extraOpts = {}) {
  return update(url, 'PATCH', data, extraOpts);
}

That’s it! The usage of these is similar to get().

import { patch, post, put } from './api.mjs';

let data = {
  'email': 'sydney@fife',
  'password': 'pistol',
};
(
  post('https://reqres.in/api/register', data)
    .then((json) => {
      console.log('------------- POST 1 success -------------');
      console.log(json);
    })
    .catch((error) => {
      console.log('------------- POST 1 failure -------------');
      console.log(error);
    })
);

data = {
  'name': 'morpheus',
  'job': 'zion resident'
};
(
  put('https://reqres.in/api/users/2', data)
    .then((json) => {
      console.log('------------- PUT 1 success -------------');
      console.log(json);
    })
    .catch((error) => {
      console.log('------------- PUT 1 failure -------------');
      console.log(error);
    })
);

data = {
  'name': 'morpheus',
  'job': 'zion resident'
};
(
  patch('https://reqres.in/api/users/2', data)
    .then((json) => {
      console.log('------------- PATCH 1 success -------------');
      console.log(json);
    })
    .catch((error) => {
      console.log('------------- PATCH 1 failure -------------');
      console.log(error);
    })
);

DELETE

Last, but not least, is DELETE. This one is pretty easy as well.

export function del(url, extraOpts = {}) {
  var opts = {
    method: 'DELETE',
    credentials: 'include',
  };
  return (
    fetch(url, Object.assign(opts, extraOpts))
      .then(handleResponse)
  );
}

One thing to note is that since delete is a reserved keyword in JavaScript, we can’t name our function that. I went with del(), but feel free to flaunt your individualism.

The usage for del() is similar, but let’s look at a different type of response that we may receive. Let’s assume that on a successful delete, the HTTP response returned is not a JSON response. This is how you would handle that:

import { del } from './api.mjs';

(
  del('https://reqres.in/api/users/2', data)
    .then(() => {
      console.log('------------- DELETE 1 success -------------');
      console.log('Success!');
    })
    .catch((error) => {
      console.log('------------- DELETE 1 failure -------------');
      console.log(error);
    })
);

Notice in then(), we don’t accept any arguments. This is because no JSON is returned. Furthermore, we don’t have to accept response as an argument and check response.ok, because that has already been done for us in handleRequest(). If response.ok were false, the catch() handler would be called instead.

Closing Thoughts

Making HTTP requests with Fetch can result in a lot of boilerplate. By leveraging traditional REST API standards, you can write generic wrapper functions that will greatly facilitate making these requests throughout your application.

For full example code, see the GitHub repository.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.