Making HTTP Requests in Elm

Want to write Elm at work? I am looking for Elm developers at diesdas.digital in Berlin. 🙂


Note: This post was written for Elm 0.16. There is now an official documentation for doing HTTP requests with the latest version.

When I learn a new programming language I like to read about all its details first. Afterwards it helps reading code written in the language that does something very simple to get a feel how all these parts fit together and how the code is organized.

When I started learning Elm I read about Signals and Tasks and how to use them in the Elm architecture, but I missed a tutorial that covers building an app from the ground up that shows how to do an HTTP request and use the StartApp package.

This is why I started writing this one while learning the language myself. I’ve built this app in three steps and published it to GitHub. In the readme you can find links to a git diff for each step that shows how the code has changed.

Goal

My goal was to create an app completely written in Elm that does the following:

  • user clicks a button
  • app makes an HTTP Request to jsonip.com
  • it parses the JSON response
  • updates the model
  • and displays the received IP address

Step 1: Basic Setup

Click here to view the full diff for step 1 on Github.

Installing Elm locally via npm

You can install Elm globally, but updating it to a new version would break all projects that depend on it. That’s why I prefer to install Elm locally for each project via npm. I started by generating a package.json with npm init and installed the Elm command line tool with npm install elm --save-dev. Then inside package.json I added a script to make it accessible via npm scripts:

{
  …
  "scripts": {
    "elm": "elm"
  }
  …
}

I also like to set up a watch command for development. You can read my post on how to compile Elm files on save without gulp or webpack to see how that’s done. (code is also included in the diff of step 1)

Installing the Elm packages

Then I executed these commands in terminal to install the packages the app needs:

npm run elm package install evancz/start-app
This package includes the StartApp function, which gets you started with the Elm architecture.

npm run elm package install evancz/elm-http
The name says it all: a package for making HTTP requests in Elm.

npm run elm package install evancz/elm-html
This one is using virtual-dom to render HTML (like React does).

npm run elm package install evancz/elm-effects
The last one is needed for executing side effects in Elm.

Creating a basic Main.elm

To test if the setup works before adding interactions, I created a basic Elm file, that just renders "Hello World":

module Main where

import Html exposing (text)

main =
  text "Hello World"

Setting up the entry html file

Now I created an index.html file, which imports the compiled Elm code. You can learn about more ways to embed Elm in the official Interop documentation.

<!DOCTYPE html>
<html>
<head>
  <title>Elm HTTP Request Example</title>
  <script src="/elm.compiled.js"></script>
</head>
<body>
  <div id="main"></div>
  <script>Elm.embed(Elm.Main, document.getElementById('main'));</script>
</body>
</html>

Compiling Elm

As I have previously set up a watch command I can now run npm run watch and save my Elm files to compile the JavaScript. Alternatively I can execute npm run elm -- make Main.elm --output elm.compiled.js to compile them just once.

Starting a local web server

To test if everything works, I started a local web server in my directory. On Mac you can do this by running:

php -S localhost:8000

Then I opened localhost:8000 in my browser to check if “Hello World” is printed on the screen.

Step 2: Adding the StartApp package

You can view the full diff of step 2 on GitHub.

Adding StartApp to Main.elm

I started by splitting up my Elm code into two files. Main.elm now only contains the small boilerplate of wiring up the Elm Architecture using StartApp and App.elm contains the actual app logic.

StartApp.start expects a record with four fields:

  • init: sets the initial values of the Model and can trigger Effects (e.g. fetching data from the backend)
  • view: renders Html with the Model it gets and wires up event handlers with an Address
  • update: updates the Model and triggers side Effects based on the Actions it gets
  • input: imports a list of Signals that trigger Actions

The main function changed to return a Signal Html, which it gets from the return value of the StartApp.start.

Ports

The last thing the Elm runtime needs to execute Effects is a port to run the tasks. Now my entry Elm file is set up so that the actual app code can do side effects.

Adding business logic in App.elm

Model

I defined a Model that only has one field to store the IP as a String.

type alias Model =
  {
    ip: String
  }

Init

The init function sets the initial IP to "Unknown" without running any side effects.

init : (Model, Effects Action)
init =
  (
    {
      ip = "Unknown"
    },
    Effects.none
  )

Update

My initial implementation of the update function only covers one Action, that does nothing.

type Action
  = DoNothing

update: Action -> Model -> (Model, Effects Action)
update action model =
  case action of

    DoNothing ->
      (model, Effects.none)

View

The view function returns a div that contains a button (without any event handling for now) and a div, which renders the ip field of the Model.

view : Address Action -> Model -> Html
view address model =
  div [] [
    button [] [ text "Get IP address" ],
    div [] [ text model.ip ]
  ]

Step 3: Make HTTP Request and decode JSON

View the full diff of step 3 on GitHub.

In the last step I updated App.elm to make the HTTP Request and deal with the JSON response.

Actions

I added two actions:

  • RequestIP: triggers the HTTP Request
  • UpdateIP: updates the model with the received IP address
type Action
  = DoNothing
  | RequestIP
  | UpdateIP (Maybe String)

Update

Below is how they are handled in the update function. The RequestIP action triggers the requestIP function, which is defined in the Effects section. UpdateIPreceives the ip, but the response might not have an IP, therefore it is wrapped in a Maybe. In case the server did not respond as expected the ip field in the Model is set to "No response".

    RequestIP ->
      (model, requestIP)

    UpdateIP ip ->
      (
        { model | ip = (Maybe.withDefault "No response" ip) },
        Effects.none
      )

View

button got a new attribute onClick, which sends the RequestIP action to StartApp’s address.

view : Address Action -> Model -> Html
view address model =
  div [] [
    button [ onClick address RequestIP ] [ text "Get IP address" ],
    div [] [ text model.ip ]
  ]

Effects

The last part of the file is doing the actual request. Http.get sends a GET request to jsonip.com. Its second argument is a function that decodes the ip field of the JSON response to a string.

The returned Task of Http.get is transformed into one that cannot fail by wrapping the result in a Maybe. This gets wrapped by the UpdateIP action via Task.map. At last Effects.task sends the task to Elm’s runtime, which actually does the request.

requestIP : Effects Action
requestIP =
  Http.get ("ip" := Json.string) "http://jsonip.com"
    |> Task.toMaybe
    |> Task.map UpdateIP
    |> Effects.task

Final words

I hope this tutorial helped you as well to better understand how side effects work in Elm. The result shows how little boilerplate code is needed in Elm compared to the current state of a typical JavaScript build process.

If you have any questions drop a comment below and I’ll try to answer them. The full code is open source on GitHub. And yes, I should use a syntax highlighter on my website that supports Elm.

Share on twitter