I’m going to analyze a practical task which is often overlooked in the Elm tutorials I’ve found so far: how to bring a Javascript module for API management to Elm.

The Javascript module I want to rewrite is almost identical to the one used in this small demo based on React and using the Stripe API for customers. It exposes four functions that allow the user to create, update, delete a single customer, and read the list of all the customers.

I assume you are already familiar with the Elm syntax and a statically typed language.

Here’s the code of the module (ES6):

It is quite simple, yet it contains some of the most common features like custom headers, form data encoding, URL parametersasynchronicitycomposability (through Promise).

Type driven development

First things first, I want to model my problem in terms of types and messages.

What does a customer look like? Which kind of messages will I send to inform the runtime that new data is available?

The customer

The customer in the application is defined as:

Since this model is going to be used across the entire application, I’ve put it in the Models.elm file.

The messages

I think that there are four main events that map 1:1 to the public interface of the Api module:

They cover the list, create, update, delete actions.

Let’s have a look to their payloads: all of them carries a Result which represents “the result of a computation that may fail“. Indeed, it can. It’s an HTTP request, after all.

The result takes the type of the error and the type of the actual result. The error will always be an HTTP.Error, while the actual result will be (based on the Stripe API documentation):

  • a list of Customer for the list function
  • a Customer for create and update
  • a string containing the id of the deleted Customer for delete

Translating to Elm

Constants

The first thing I do with Elm is creating the new file Api.elm and define the constants to be used in the API:

Notice that I don’t add the Content-Type header since it is added by elm based on the type of the body.

Since Elm doesn’t have string interpolation, I also add a helper for URLs with the id:

Helper functions

Now it’s time of the helper’s functions encode and normalizeData, used to encode the form. The biggest difference with Javascript is that at this point we need a type for the customer: while in Javascript customer is just a plain object, the Elm compiler calls for a more specific definition.

So far, the mapping of Javascript to Elm is quite straightforward, apart from types and the syntax.

HTTP custom requests

This is the part where Elm gets a bit more verbose: in Javascript, using the fetch API, I can perform a customized request just defining the options as keys of an object which can either have a value or be undefined.

In Elm, everything has a type, thus need to be explicitly defined.

Let’s break it down.

First of all, I’m using the Http.request method which is used to perform a non-trivial request. It takes a record with the settings, which resembles the second argument or fetch: the main difference is that all the settings must be declared even if they are False or Nothing.

In Elm, the concept of falsy doesn’t exist.

In order to be able to express all of my actions (create, read, update, delete) I want my requests to be customizable in terms of method, URL, and body. Thus, I pass the request creator those parameters, plus the headers. I also need to tell the Http engine which kind of response I expect to receive from the API.

This is a huge move from the Javascript world, even if you are used to typings (using Typescript, or Flow): there is no escape hatch like any. Either I declare that exact type or it won’t compile.

I know from the API specification that JSON is returned, and this is why I use the expectJson function to declare it. But it’s not enough. To the compiler, telling that something is JSON is still too generic: the function which expects JSON wants Decoder which will be used to get a proper Elm type ready to flow into my runtime.

That’s why there is an extra parameter, the resultDecoder, to keep the makeRequest function as generic and flexible as possible: if I pass it a Decoder of “a” I will get a Request of “a”. Nothing different.

The JSON decoder

A JSON decoder for the type a is a function that takes a JSON value and gives back an item of type a. The problem with it is that in Elm it is quite verbose creating decoder for most of the types.

This is why I’m using the elm-decode-pipeline library from NoRedInk, which allows me to use the pipes to concatenate multiple JSON fields:

What this code is saying is:

  • produce a Customer
    • getting a required field of type float from the key account_balance
    • getting an optional field of type string from the key description and using an empty string if not found

For the exact content of the server’s response, please refer to the Stripe API.

Note also that since Customer is a type alias for a record, it can be used as a constructor of that record: the decoder will literally build the Customer with Customer account_balance description email firstName id lastName. In this example, my Customer definition is weak because I could mess up with some of the arguments’ order without getting an actual error since they are almost all strings.

Implementing the public functions

What is left to be implemented is the public API of my module: the function which will be called by other modules in the application.

Let’s start with creating a customer:

The function create receives a Customer. It encodes the customer’s fields using the helpers and bundles it in a body which is a string representing a form using the application/x-www-form-urlencoded format. This is why I’ve skipped the Content-type header: the elm runtime will add it for us based on this information.

Secondly, a request is created using the helper and following the Stripe API specification for creating a customer. Since I expect the created customer to be returned by the server, I’m passing the customerDecoder.

Lastly, I use the Http.send function to create a command which will perform the request and send the CustomerCreated message with the payload.

Note that no request is performed, yet. Working with a declarative approach, I don’t order to send an HTTP request, rather I just describe it and it will be performed by the runtime.

Updating a customer is pretty straightforward based on what we already have:

Indeed, it is almost the same code for creating it but with a different URL and message.

JSON inside JSON inside…

The function which asks the server for all the customers has a new hindrance. Based on the API documentation, the list is delivered inside the field data of the JSON response.

I need a new decoder, which will extract a list of Customer from that field:

Here I’m using the standard Decoder library because I don’t have multiple fields to pick up but just one: the data. The syntax is similar though and shouldn’t be hard to read after the customerDecoder implementation.

With the new decoder, I can finally implement the readAll function (I had to change its name because I have list in the scope from the Json.Decode module).

Following the same logic, I can implement the delete function which deletes a customer:

Here the decoder is so simple that I don’t feel the need to write it in its own function: it will just look for the id field and try to parse it as a string.

The bottom line

Generally, Elm is more verbose compared to ES6: it calls for an initial types definition, plus some time to think about messages and the data flow.

On the other hand, there’s a hidden overhead with Javascript. For example, you can declare the request function as a one-liner, like in:

However, you are left with the management of the input and the result at a certain point: the later you do it, the more defensive (and duplicated) code you will produce.

My opinion is that the application’s complexity is just shifting towards a specific point, rather than being spread across the view.

Since Elm forces you to declare and handle types, it is safer: the Customer will always be a full fledged Customer with all of its properties. The advantage is that, while it comes with a bit more complexity defining the module for API interaction, any data coming from that module is trusted to be of a specific type.