Iterative Development in Elm: HTTP

Joël Quenneville

Elm’s compiler is a great assistant. We can make HUGE changes to our code and it will make sure we make all the necessary fixes to get it working again. It gives us complete freedom to refactor.

Just because we can doesn’t mean we should. I’ve found it’s often much nicer to take a lot of small steps, each of which leave us in a compiling state. Recently, I’ve been using incremental approach a lot for interactions involving HTTP requests, especially complex GraphQL queries.

In particular, faking out HTTP requests with a sleep allows me to hand-wave the request away while I deal with the various UI states (loading, success, failure), all while remaining in a compiling state.

Setup

We’ve introduced a button. When we click it we want to fetch a user from the API via HTTP.

button [ onClick UserClickedGetUser ]
  [ text "Get the user" ]

Now we’re getting a compilation error because there’s a new message we aren’t handling. ❌

Do nothing – inline

We add a new case to our update function. The simplest thing we can do is not to make the HTTP request. We set the model into the loading state and return Cmd.none. Now we’re compiling again! ✅

We should be able to click the button and see the UI go into a loading state.

-- In the `update` function

UserClickedGetUser ->
  ({ model | user = Loading }, Cmd.none)

Do nothing – extract method

The next simplest thing we can do is to extract method. We’re still not doing anything, but now it’s happening elsewhere. We’re still compiling. ✅

-- In the `update` function

UserClickedGetUser ->
  ({ model | User = Loading }, getUser)
getUser : Cmd Msg
getUser =
  Cmd.none

Sleep

Time to actually do some work in the command. In an attempt to tighten the incremental steps we need to get to a compiling state, we’d like to skip making an actual HTTP request for now. Let’s do something simpler instead. We can use a combination of Process.sleep and Task.perform to create a command that returns the same kind of responses that an HTTP request would.

getUser : Cmd Msg
getUser =
  Process.sleep 2000
    |> Task.perform (\_ ->
      ReceivedUserFromServer (Ok fakeUser)
    )

fakeUser : User
fakeUser =
  { name = "Fake", age  = 42 }

Now the build is broken because we aren’t handling the new message ReceivedUserFromServer. ❌

Handle Result

Back in our update function, we add branches for both success and failure cases of our message. We probably also need to handle success and errors views for those states. Now we’re compiling again! ✅

We should now be able to click the button, see a loading state, then after a couple of seconds see a success or error state. All without making any HTTP requests!

-- In the `update` function

UserClickedGetUser ->
  ({ model | user = Loading }, getUser)

ReceivedUserFromServer (Ok user) ->
  -- HANDLE SUCCESS

ReceivedUserFromServer (Err error) ->
  -- HANDLE ERROR

Real HTTP

Now that we’ve got all the UI interactions done, we can finally circle back to the HTTP request. We don’t need to worry about loading or error states. Those are already handled. All we need to do here is make the API call. Once that compiles, we’re done! ✅

getUser : Cmd Msg
getUser =
  -- DO REAL HTTP HERE

See it in action

I’ve packaged the example above into a live Ellie demo.

Fake HTTP request

Conclusion

I’ve found a lot of benefits to the incremental approach. Not only does it help focus my development process, but it also allows me to scope commits more tightly. I can actually run the app and interact with the HTML at almost any point in the development process.

As a bonus, this approach also allows me to develop against an API endpoint that doesn’t exist yet. This isn’t uncommon when working with a separate backend team that’s building features in parallel.

So the next time you’re integrating some API, throw a few sleeps into your code. You may find it’s a winning combo 🥇