Fetch - from simple to scalable implementation
Tags: javascript, programming, tutorial, productivity
Published: 2021-08-21 13:37:41


#### Hey there! 👋

I was bored and wanted to write something. I've ended up with this, a step-by-step guide on how I approach a task, from the most basic to the best fitting implementation for the needs.

### What will I build?
Piece of code for fetching data. It will be fetching a jokes API, which returns either a list of jokes or a random joke.

I will then try to improve it step-by-step, until I have a solid and scalable base.

### Context
Nothing is built except for the API, the task is to create a way of fetching the jokes so that the UI team can start doing their job.

### Initial implementation

Most simple scenario would be to create some sort of function that fetches all the jokes, and one that fetches a random one. Easy enough, let's see how that works:

```js
function fetchAllJokes() {
    return fetch('https://my-api.com/jokes')
        .then(response => response.json());
}

function fetchRandomJoke() {
    return fetch('https://my-api.com/jokes/random')
        .then(response => response.json());
}
```

As you can see, this would immediately work, and let the UI team do their job right now. But it's not very scalable, let's see how to improve on this without breaking anything for the UI team.

### Iteration 1

We know that for now, we can only get jokes, but we also know that most likely this API will expand in the future. And we will need to implement other stuff, like creating/updating jokes. Fetching other resources, etc...


One thing I try to remind myself before I start to build or design a feature is:
> "can I make it so I don't have to touch this code again?"

Most times the answer is **yes**, by kind of using the open-close principle, which states, that a function/method/class should be open for extension but closed to modification. 

Another rule I try to apply to myself is, **work yourself upwards**. What I mean is, start from the most simple, low-level functionality, and then start building on top of that.

In this case the lowest level functionality is executing fetch, with a set of options. So I start by defining a custom function around fetch:

```js
function fetcher(url, options = {}) {
    return fetch(url, {
        method: HttpMethods.GET,
        ...options,
    });
}
```

It's mostly the same as calling fetch directly, but, with a difference: 

* **It centralizes where fetch is called**, instead of calling fetch directly in several places in the app, we only use it in the fetcher function. 

* **It's easier to change/modify** in case fetch API changes or we want to do something before or after each fetch request. _Though I would resist it if it can be avoided as you will see later on in the post_.


Now that we have this base, we can start building on top of it. Let's make it possible to use the most common HTTP methods, like POST, PUT, GET, DELETE. 

```js
function fetcherPost(url, options = {}) {
    return fetcher(url, {
        ...options,
        method: HttpMethods.POST,
    });
}

function fetcherPut(url, options = {}) {
    return fetcher(url, {
        ...options,
        method: HttpMethods.PUT,
    });
}
// ...
```
I think you get the gist of it. We create a function for each method.

We would use it as follows:
```js
function fetchAllJokes() {
    return fetcherGet('https://my-api.com/jokes')
        .then(response => response.json());
}

function fetchRandomJoke() {
    return fetcherGet('https://my-api.com/jokes/random')
        .then(response => response.json());
}
```

This is ok, but we can do better.

### Iteration 2

The API uri will probably be the same in all requests, and maybe other ones too. So let's store that in an env variable:

```js
function fetchAllJokes() {
    return fetcherGet(`${env.API_URL}/jokes`)
        .then(response => response.json());
}
```

Better, now you can see that converting response to JSON is also being repeated. How could we improve this? 

First, let's see how **NOT TO DO IT**, which would be to just add it to the fetcher function, in the end, all requests pass through it, right?

```js
function fetcher(url, options = {}) {
    return fetch(url, {
        method: HttpMethods.GET,
        ...options,
    })
    .then(response => response.json());
}
```

```js
function fetchAllJokes() {
    return fetcherGet(`${env.API_URL}/jokes`);
}
```

Yes, we get rid of it in the `fetchAllJokes` function, but what if a request does not return JSON? 

We would need to then remove it from the fetcher, and add it again to only those requests that return JSON. Wasting time changing stuff we have already done, and remember the rule "can I make it so I don't have to touch the code I write again?".


Now let's see **HOW TO DO IT**:
> I want to express that there are always many correct ways to solve a problem, this is one of them. It's in no way the only or best solution

One option would be to extract the functionality into a function, for example:
```js
function jsonResponse(response) {
    return response.json();
}

// Then we could use it as follows
function fetchAllJokes() {
    return fetcherGet(`${env.API_URL}/jokes`).then(jsonResponse);
}

// And if we receive other format
function fetchAllJokes() {
    return fetcherGet(`${env.API_URL}/jokes`).then(xmlResponse);
}
```

This is a good approach, as it lets us process the response afterward, depending on the data returned.

We could even extend the fetcher function, for each data format:

```js
function jsonFetcher(url, options = {}) {
    return fetcher(url, options).then(jsonResponse);
}

function xmlFetcher(url, options = {}) {
    return fetcher(url, options).then(xmlResponse);
}
```

This approach is even better in some senses, as we can check things like headers, body, etc on each request...

For example, we want to ensure that, with **json** requests, a header of type `'application/json'` is sent. 

```js
function jsonFetcher(url, options = {}) {
    const isPost = options.method === HttpMethods.POST;
    const hasHeaders = options.headers != null;

    if (!hasHeaders) options.headers = {};

    if (isPost) {
        options.headers['Content-Type'] = 'application/json';
    }

    return fetcher(url, options).then(jsonResponse);
}
```

Now, any time a post request is made with `jsonFetcher`, the content-type header is always set to `'application/json'`.

BUT and a big BUT, with this approach, you might have spotted a problem. We now have to create new functions for each method (`fetcherGet`, `fetcherPost`), for each fetcher...

## Iteration 3

This could be improved by rethinking how we create fetchers, instead of overriding the fetcher function, we could return an object, containing all methods for that specific fetcher.

One solution to this problem would be to create a function, which receives a fetcher, and returns an object with all methods attached:
```js
function crudForFetcher(fetcher) {
    return {
        get(url, options = {}) {
            return fetcher(url, {
                ...options,
                method: HttpMethods.GET,
            })
        },
        post(url, options = {}) {
            return fetcher(url, {
                ...options,
                method: HttpMethods.POST,
            })
        },
        // ...more methods ...
    }
}

// Create fetch for each fetcher type
const fetchDefault = crudForFetcher(fetcher);
const fetchJson = crudForFetcher(jsonFetcher);
const fetchXml = crudForFetcher(xmlFetcher);

fetchJson.get('my-api.com/hello');
```

There's still a thing that's bugging me a bit, it is that we need to pass the full API URI in each request, now it's really simple to add this functionality as we have it all broke down. 


What we can do is improve the `crudForFetcher` function a bit more, by making it receive some options:

```js
function crudForFetcher(fetcher, options = { uri: '', root: '' }) {
    const { uri, root } = options;

    return {
        get(path, options = {}) {
            return fetcher(path.join(uri, root, path), {
                ...options,
                method: HttpMethods.GET,
            })
        },
        // ... more methods ...
    }
}

const jokesFetcher = crudForFetcher(
    jsonFetcher, 
    { 
        uri: env.API_URL, 
        root: `jokes` 
    }
);
```

What this change does, is, merges the URI, root, and path of a specific request, into a single URI. 

In the case of `jokesFetcher`, the URI for the requests will always start with `https://my-api.com/jokes`. 

We can now safely replace our original functions, without the UI team needing to change anything, but we now have a lot more power and ready to scale, yay!!!

```js
function fetchAllJokes() {
    return jokesFetcher.get(); // `https://my-api.com/jokes`
}

function fetchRandomJoke() {
    return jokesFetcher.get('/random'); // `https://my-api.com/jokes/random`
}
```

As you can see, we have not modified anything we've built, except for `crudForFetcher`.

#### Everything put together

```js
function fetcher(url, options = {}) {
    return fetch(url, {
        method: HttpMethods.GET,
        ...options,
    });
}

function jsonResponse(response) {
    return response.json();
}

function jsonFetcher(url, options = {}) {
    return fetcher(url, options).then(jsonResponse);
}

function crudForFetcher(fetcher, options = { uri: '', root: '' }) {
    const { uri, root } = options;

    return {
        get(path, options = {}) {
            return fetcher(path.join(uri, root, path), {
                ...options,
                method: HttpMethods.GET,
            })
        },
        post(path, options = {}) {
            return fetcher(path.join(uri, root, path), {
                ...options,
                method: HttpMethods.POST,
            })
        },
    }
}

// Exposed API
const fetchJokes = crudForFetcher(
    jsonFetcher, 
    { 
        uri: env.API_URL, 
        root: `jokes` 
    }
);
function fetchAllJokes() {
    return jokesFetcher.get(); 
}

function fetchRandomJoke() {
    return jokesFetcher.get('/random');
}
```


## Summary

We've taken a simple implementation, and bit by bit, building up until we have something that will scale quite well, without breaking anything along the way (with a bit more refinement work of course).

I've been using this approach for the last couple our years, in a variety of projects, frameworks, languages, etc... and it's working out pretty well for me.

It's also been really productive, in the sense that it has reduced the amount of work I need to do significantly.

And just to reiterate, this is one approach of many that could work in this scenario. I might post a different approach using oop.

**What to take out of this:**
* Understand the task at hand
* Look at the forest, not just the trees (don't just implement the feature, think about it, and the stuff around it)
* Build things progressively, but not recklessly
* Make functions/methods as closed as possible
* Keep things simple

----

I really enjoyed writing this, and I hope you like the read as well!

###### If you did, consider supporting me by reacting to the post, following me here or over on GitHub, or commenting! :heart: