Encouraging proper API response handling on code level
By: Dan Homola
Tags: JavaScript, Typescript, Functional Programming

If you ever developed any application that communicates with a REST API, you probably know that handling the responses can be a bit tricky. It is often easy to forget to handle for example error states. In this article, we’ll show an approach that we devised to make forgetting this much less probable. The solution described uses TypeScript, but the core concept should be usable in other languages as well.

Previous solution

Not long after we started developing React Single Page apps, we found out that there are several things to keep track of for every REST API resource:

whether there is a request pending (in other words if we are currently waiting for a server response)

whether there was an error last time we tried to call the API endpoint

when the last change of this object happened (usually the time of the last response)

the contents of the last successful response

For this, we created a small library called loadable (this was before the popular react-loadable pacakge was published, so the name clashes now). Its main purpose was to facilitate and unify representations of REST resources. The core object called Loadable can be seen bellow:

Old version of Loadable interface

As we can see, it has four properties — one for each thing we want to track mentioned above. It uses a generic parameter for the data property to allow representation of any REST resource in a type-safe way.

There was also a function for creating the instances that handled the timestamp assignment:

createLoadable method

There is nothing wrong with this representation itself, in fact it is used as is in the new approach (see below). The problem is, there is no standard way to consume this object that would enforce that the consumer handles all the possible states the object can be in. Most of the time, there was code like this when handling Loadable instances in React code:

Old version of render method

This was mostly fine, however, there are two main problems with this:

forgetting to handle one of the states handling of state combinations gets really messy

It is easy to forget to handle one of the states (for us it was the error most of the time). This leads to suboptimal UX, because when a REST request fails, the app seems to not work at all.

Also, what if we wanted to display the previously received data as well as the loading indicator (for example when the user explicitly reloads the resource)? The logic gets really complex in those cases, and is super easy to make an error in.

The new approach aims to alleviate both of these issues.

Current solution

Recently I finally read the brilliant Professor Frisby’s Mostly Adequate Guide to Functional Programming. Despite its wacky title, it is a really useful explanation of various functional programming concepts. You should definitely check it out if you haven’t already. Chapter 8 discusses various containers and how they can help you with for example error handling. This inspired me to revise our loadable package and attempt to use the approaches described to fix our problems.

It turns out, both of the problems mentioned above can be solved by a simple form of “pattern matching”. I put it in quotes, because it is no pattern matching per se, it shares the basic idea with it, though.

Instead of the createLoadable function, there is now a loadable utility object that looks like this:

loadable.of method

The name of the instance creating method ( of ) is inspired by the Guide, but it does the same thing as the createLoadable function used to. The interesting bit is the merge method:

loadable.merge method

As we can see, the match method uses a configuration object of the LoadablePattern<T> type. This type consists of handlers of all the states that the Loadable instance can be in.

The important thing to notice is that some of the handlers are marked as required (more precisely not marked as optional with the ? ). This means that every caller of the merge method has to provide handlers for the basic states: loading in progress, data retrieved, error occurred, and the default empty state (when the resource has not been requested yet for the first time). Thus, we solve the first problem, if using the merge method, all the basic states are handled.

The second problem is solved by encapsulating the decision tree into one place. The caller can also specify more nuanced handlers. For example loadingWithData which represents a situation when we already had some data but are loading it again – hence we show the current data as well as the loading indicator.

The matching is opinionated regarding the priority in which the cases are matched. This is necessary as TypeScript does not have proper pattern matching (yet) which would allow the consumer to define the priority themselves using the order of the clauses. For our use cases this is fine though, as it means we handle the diferent states consistently across the app.

Using these methods is then as easy as:

New version of render method

As we can see, it allows us to extend the initial example by providing loadingWithData handler while retaining its previous functionality.

Design decisions

Observant reader might have two questions about this implementation:

Why is the merge method curried? Why is the merge method implemented as an instance method of the Loadable type?

The reason for the currying is illustrated in the usage example above. We can declare the matcher once, instead of doing it on every render. This keeps the render method as lightweight as possible.

As to why the Loadable is not a class with merge as its instance method (and of as a static method) similarly to for example Either , the main reason is serializability.

Most of our TypeScript applications are React apps that use Redux as the store management solution (though I really want to look into MobX soon). All of the Loadable instances we use are therefore stored in a Redux store. As you may know it is really beneficial if all the data stored in the store is serializable. This unfortunately means we cannot use the "proper" class approach easily.

If we were to use loadable in some other environment, I would definitely go for the class approach, as it avoids the Loadable / loadable dichotomy.

Summary

In this article we’ve shown how to enforce a certain practice on code level at compile time using functional approaches. Hopefully, this will inspire you to solve some of your code problems in a similar way.