1. Fetch () is good, but you may wish for better

The FETCH () API allows you to perform network requests in web applications.

The use of fetch() is very simple: call fetch(‘/movies.json’) to start the request. When the request completes, you get a Response object from which to extract the data.

Here is a simple example of how to get jSON-formatted data from the movies.json URL:

async function executeRequest() {
  const response = await fetch('/movies.json');
  const moviesJson = await response.json();
  console.log(moviesJson);
}

executeRequest(); 
// logs [{ name: 'Heat' }, { name: 'Alien' }]

Copy the code

As shown in the code snippet above, the JSON object must be extracted manually from the response :moviesJson = await Response.json (). Just do it once, no problem. But if your application is performing many requests, all the time it takes to extract json objects with await Response.json () is tedious.

Therefore, a third-party library, such as AXIos, is often used, which can greatly simplify the processing of requests. Consider using Axios to get the same movie:

async function executeRequest() {
  const moviesJson = await axios('/movies.json');
  console.log(moviesJson);
}

executeRequest(); 
// logs [{ name: 'Heat' }, { name: 'Alien' }]
Copy the code

MoviesJson = await axios(‘/movies.json’) returns the actual JSON response. You don’t have to manually extract JSON as fetch() requires.

However, using a helper library like Axios also brings some problems:

First, it increases the bundle size of the Web application. Second, your application combines with a third-party library: you get all the benefits of that library, but you also get all its bugs.

My goal was to take a different approach that got the best results from both — using the decorator pattern to increase the ease of use and flexibility of the FETCH () API.

The idea is to wrap a base FETCH class (I’ll show you how to define it) for whatever other functionality you need: extracting JSON, timeouts, throwing errors in bad HTTP states, handling Auth headers, and so on. Let’s see how to do this in the next section.

2. Prepare the Fetcher interface

The decorator pattern is very useful because it enables the addition of functionality (in other words – decoration) on top of the basic logic in a flexible and loosely coupled manner.

If you’re not familiar with the decorator pattern, I suggest you read how it works.

Applying a decorator to enhance fetch() requires a few simple steps:

  • The first stepIs to declare a name calledFetcherAbstract interface to:
type ResponseWithData = Response & { data? : any }; interface Fetcher { run( input: RequestInfo, init? : RequestInit ):Promise<ResponseWithData>;
} 
Copy the code

The Fetcher interface has only one method, which takes the same parameters and returns the same data type as regular fetch().

  • The second stepIs fundamental to implementationfetcherClass:
class BasicFetcher implements Fetcher { run( input: RequestInfo, init? : RequestInit ):Promise<ResponseWithData> {
    returnfetch(input, init); }}Copy the code

BasicFetcher implements the Fetcher interface. One of its methods, run(), calls the regular fetch() function.

For example, let’s use the basic fetcher class to get a list of movies:

const fetcher = new BasicFetcher();
const decoratedFetch = fetcher.run.bind(fetcher);

async function executeRequest() {
  const response = await decoratedFetch('/movies.json');
  const moviesJson = await response.json();
  console.log(moviesJson);
}

executeRequest(); 
// logs [{ name: 'Heat' }, { name: 'Alien' }]
Copy the code

Const fetcher = new BasicFetcher() creates an instance of the fetcher class. DecoratedFetch = fetcher.run.bind(fetcher) creates a binding method.

You can then use decoratedFetch(‘/ movies.json ‘) to fetch the movie JSON, just as you would with regular fetch().

In this step, the BasicFetcher class provides no benefit. Also, things get more complicated with new interfaces and new classes! Wait a moment and you’ll see the great benefits that come when the decorator pattern is introduced into action.

3. Add a decorator to the method that extracts JSON data

The main decorator pattern is the decorator class.

Decorator classes must conform to the Fetcher interface, wrap the decorated instance, and introduce additional functionality in the run() method.

Let’s implement a decorator that extracts JSON data from the response object:

class JsonFetcherDecorator implements Fetcher {
  private decoratee: Fetcher;

  constructor (decoratee: Fetcher) {
    this.decoratee = decoratee;
  }

  asyncrun( input: RequestInfo, init? : RequestInit ):Promise<ResponseWithData> {
    const response = await this.decoratee.run(input, init);
    const json = await response.json();
    response.data = json;
    returnresponse; }}Copy the code

Let’s take a closer look at how the JsonFetcherDecorator is constructed.

JsonFetcherDecorator conforms to the Fetcher interface.

JsonExtractorFetch has a private field, decoratee, which also conforms to the Fetcher interface. In the run() method this.decoratee.run(input, init) performs the actual data fetch.

Json = await Response.json () then extracts the JSON data from the response. Finally, the response. Data = JSON The extracted JSON data is assigned to the response object.

Now let’s compose the BasicFetcher with the JsonFetcherDecorator decorator and simplify the use of fetch() :

const fetcher = new JsonFetcherDecorator(
  new BasicFetcher()
);
const decoratedFetch = fetcher.run.bind(fetcher);

async function executeRequest() {
  const { data } = await decoratedFetch('/movies.json');
  console.log(data);
}

executeRequest(); 
// logs [{ name: 'Heat' }, { name: 'Alien' }]
Copy the code

Instead of manually extracting JSON data from the response, you can now access the extracted data from the data property of the response object.

By moving the JSON extractor to the decorator, you now don’t have to manually extract the JSON object anywhere const {data} = decoratedFetch(URL) is used.

4. Create request timeout decorator

By default, the FETCH () API times out at a browser-specified time. In Chrome, the timeout time for web requests is 300 seconds, while in Firefox it is 90 seconds.

Users can wait eight seconds to complete a simple request. This is why you need to set a timeout for network requests and notify users of network problems after 8 seconds.

The great thing about the decorator pattern is that you can decorate your base implementation with as many decorators as you want! So, let’s create a timeout decorator for the fetch request:

const TIMEOUT = 8000; // 8 seconds

class TimeoutFetcherDecorator implements Fetcher {
  private decoratee: Fetcher;

  constructor(decoratee: Fetcher) {
    this.decoratee = decoratee;
  }

  asyncrun( input: RequestInfo, init? : RequestInit ):Promise<ResponseWithData> {
    const controller = new AbortController();
    const id = setTimeout(() = > controller.abort(), TIMEOUT);
    const response = await this.decoratee.run(input, { ... init,signal: controller.signal
    });
    clearTimeout(id);
    returnresponse; }}Copy the code

The TimeoutFetcherDecorator is a decorator that implements the Fetcher interface.

Inside the Run () method of the TimeoutFetcherDecorator: If the request does not complete within 8 seconds, abort the request using the abort controller.

Now let’s use this decorator:

const fetcher = new TimeoutFetcherDecorator(
  new JsonFetcherDecorator(
    new BasicFetcher()
  )
);
const decoratedFetch = fetcher.run.bind(fetcher);

async function executeRequest() {
  try {
    const { data } = await decoratedFetch('/movies.json');
    console.log(data);
  } catch (error) {
    // Timeouts if the request takes
    // longer than 8 seconds
    console.log(error.name);
  }
}

executeRequest(); 
// if the request takes more than 8 seconds
// logs "AbortError"
Copy the code

In this example, the request to /movies.json takes more than 8 seconds.

DecoratedFetch (‘/movies.json’) throws a timeout error due to the TimeoutFetcherDecorator.

The basic getter is now wrapped in two decorators: one to extract the JSON object and the other to request a timeout within 8 seconds. This greatly simplifies the use of decoratedFetch() : When decoratedFetch() is called, the decorator logic will work for you.

5. To summarize

The FETCH () API provides basic functionality for performing fetch requests. But you need more than that. Fetch () alone forces you to manually extract JSON data from the request, configure timeouts, and so on.

To avoid boilerplate files, you can use a friendlier library like Axios. However, using a third-party library like Axios will increase the size of your application package, and you’ll be tightly integrated with it.

Another solution is to apply the decorator pattern on fetch(). You can create decorators that extract JSON from requests, timeout requests, and so on. You can combine, add, or remove decorators at any time without affecting the code that uses them.